fix: 100% coverage, WCAG accessibility fixes, and documentation updates for per-user OAuth wizards
Test coverage: - Add config=None branch test for dropbox, onedrive, google_drive views (100% coverage) - Add WATCH_FOLDER source-type test (folder_path vs folder key) - Add integration-not-found fallback-to-admin-mode test WCAG 2.1 AA fixes: - Add aria-labelledby="modalTitle" to role="dialog" modals in setup templates - Add aria-hidden="true" to decorative SVGs in callback templates - Add role="status" aria-label="Loading" to spinner divs - Add aria-live="polite" to processing-message and success/folder-selection regions - Add role="alert" aria-live="assertive" to error containers - Update "Return to Setup" link to preserve integration_id in user mode Docs: update DropboxSetup.md, GoogleDriveSetup.md, OneDriveSetup.md with per-user OAuth flow section Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+27
-13
@@ -15,23 +15,37 @@ For a complete list of configuration options, see the [Configuration Guide](Conf
|
|||||||
|
|
||||||
## Setup Methods
|
## Setup Methods
|
||||||
|
|
||||||
You can set up Dropbox integration in two ways:
|
DocuElevate supports two distinct Dropbox OAuth flows:
|
||||||
|
|
||||||
1. **Using the Built-in Setup Wizard (Recommended)**: An interactive setup experience available at `/dropbox-setup` in the web interface
|
1. **Per-User Integration Wizard (Recommended for end users)**: Triggered from the Integrations dashboard (`/integrations`) by clicking **Authorize** on a Dropbox destination or Dropbox-backed Watch Folder. Credentials are saved securely to your personal integration record — global settings are never exposed.
|
||||||
2. **Manual Setup**: Following the step-by-step instructions in this document
|
2. **System-Level Setup Wizard**: Available at `/dropbox-setup` for administrators configuring the global system-wide Dropbox connection. Generates environment variables for all worker nodes.
|
||||||
|
3. **Manual Setup**: Following the step-by-step instructions in this document.
|
||||||
|
|
||||||
## Using the Setup Wizard
|
## Per-User OAuth Flow (Integrations Dashboard)
|
||||||
|
|
||||||
The easiest way to set up Dropbox integration is to use the built-in setup wizard:
|
End users authorize their own Dropbox integration from the **Integrations** dashboard:
|
||||||
|
|
||||||
1. Navigate to the `/dropbox-setup` page in your DocuElevate instance
|
1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
|
||||||
2. Follow the on-screen instructions to create a Dropbox app
|
2. Create a Dropbox destination integration (or a Watch Folder with `source_type = dropbox`).
|
||||||
3. Enter your App Key and App Secret in the wizard
|
3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration.
|
||||||
4. Optionally specify a custom folder path for uploads
|
4. Enter your Dropbox App Key and App Secret in the wizard (or use the global admin credentials if pre-configured).
|
||||||
5. Click "Start Authentication Flow" to begin the authorization process
|
5. Click **Start Authentication Flow**, authorize access in Dropbox, and the refresh token is automatically saved to your personal integration record.
|
||||||
6. Complete the Dropbox authentication process
|
6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||||
7. The system will automatically exchange the authorization code for a refresh token
|
|
||||||
8. Copy the generated environment variables for your worker nodes
|
> **Note:** Your credentials are stored encrypted per-integration and are never mixed with other users' data. Each user can have multiple Dropbox integrations with independent tokens.
|
||||||
|
|
||||||
|
## Using the System-Level Setup Wizard (Admin)
|
||||||
|
|
||||||
|
The easiest way to configure the global Dropbox integration is to use the built-in setup wizard:
|
||||||
|
|
||||||
|
1. Navigate to the `/dropbox-setup` page in your DocuElevate instance.
|
||||||
|
2. Follow the on-screen instructions to create a Dropbox app.
|
||||||
|
3. Enter your App Key and App Secret in the wizard.
|
||||||
|
4. Optionally specify a custom folder path for uploads.
|
||||||
|
5. Click **Start Authentication Flow** to begin the authorization process.
|
||||||
|
6. Complete the Dropbox authentication process.
|
||||||
|
7. The system will automatically exchange the authorization code for a refresh token.
|
||||||
|
8. Copy the generated environment variables for your worker nodes.
|
||||||
|
|
||||||
The wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment.
|
The wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment.
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,28 @@ This guide explains how to set up the Google Drive integration for DocuElevate.
|
|||||||
|
|
||||||
For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md).
|
For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md).
|
||||||
|
|
||||||
## Authentication Methods
|
## Authentication Methods and Setup Flows
|
||||||
|
|
||||||
DocuElevate supports two authentication methods for Google Drive:
|
DocuElevate supports two authentication methods for Google Drive:
|
||||||
|
|
||||||
1. **OAuth Authentication (Recommended)** - User-based authentication that provides better security and control. Recommended for most deployments.
|
1. **OAuth Authentication (Recommended)** - User-based authentication that provides better security and control. Recommended for most deployments.
|
||||||
2. **Service Account Authentication** - Server-to-server authentication that doesn't require user interaction. Useful for specific enterprise deployments.
|
2. **Service Account Authentication** - Server-to-server authentication that doesn't require user interaction. Useful for specific enterprise deployments.
|
||||||
|
|
||||||
## Method 1: OAuth Authentication Setup (Recommended)
|
### Per-User OAuth Flow (Integrations Dashboard)
|
||||||
|
|
||||||
|
End users can authorize their own Google Drive integration directly from the **Integrations** dashboard — no admin involvement required:
|
||||||
|
|
||||||
|
1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
|
||||||
|
2. Create a Google Drive destination integration (or a Watch Folder with `source_type = google_drive`).
|
||||||
|
3. Click the **Authorize** button — it opens the OAuth wizard pre-loaded with your integration's configuration.
|
||||||
|
4. Enter your Google OAuth Client ID and Client Secret in the wizard.
|
||||||
|
5. Click **Start Authentication Flow** and authorize access in Google.
|
||||||
|
6. Credentials are saved automatically to your personal integration record; the page redirects back to `/integrations`.
|
||||||
|
7. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||||
|
|
||||||
|
> **Note:** Credentials are stored encrypted per-integration. Each user can hold multiple Google Drive integrations with independent tokens targeting different folders or accounts.
|
||||||
|
|
||||||
|
## Method 1: OAuth Authentication Setup (System-Level)
|
||||||
|
|
||||||
The OAuth method is preferred as it:
|
The OAuth method is preferred as it:
|
||||||
- Provides better security with token expiration and refresh
|
- Provides better security with token expiration and refresh
|
||||||
|
|||||||
+26
-12
@@ -16,22 +16,36 @@ For a complete list of configuration options, see the [Configuration Guide](Conf
|
|||||||
|
|
||||||
## Setup Methods
|
## Setup Methods
|
||||||
|
|
||||||
You can set up OneDrive integration in two ways:
|
DocuElevate supports two distinct OneDrive OAuth flows:
|
||||||
|
|
||||||
1. **Using the Auth Wizard (Recommended)**: An interactive setup experience available at `/onedrive-setup` in the web interface
|
1. **Per-User Integration Wizard (Recommended for end users)**: Triggered from the Integrations dashboard (`/integrations`) by clicking **Authorize** on a OneDrive destination or OneDrive-backed Watch Folder. Credentials are saved securely to your personal integration record.
|
||||||
2. **Manual Setup**: Following the step-by-step instructions in this document
|
2. **System-Level Setup Wizard**: Available at `/onedrive-setup` for administrators configuring the global system-wide OneDrive connection.
|
||||||
|
3. **Manual Setup**: Following the step-by-step instructions in this document.
|
||||||
|
|
||||||
## Using the Auth Wizard
|
## Per-User OAuth Flow (Integrations Dashboard)
|
||||||
|
|
||||||
The easiest way to set up OneDrive integration is to use the built-in auth wizard:
|
End users authorize their own OneDrive integration from the **Integrations** dashboard:
|
||||||
|
|
||||||
1. Register an application in Azure AD (see steps below)
|
1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
|
||||||
2. Navigate to the OneDrive Setup page at `/onedrive-setup`
|
2. Create a OneDrive destination integration (or a Watch Folder with `source_type = onedrive`).
|
||||||
3. Enter your Client ID and other required information
|
3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration.
|
||||||
4. Click "Start Authentication Flow"
|
4. Enter your Azure AD Client ID and Client Secret in the wizard.
|
||||||
5. Complete the Microsoft authentication process
|
5. Click **Start Authentication Flow**, authorize access via Microsoft, and the refresh token is automatically saved to your personal integration record.
|
||||||
6. The system will automatically exchange the authorization code for a refresh token
|
6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||||
7. Copy the generated environment variables for your worker nodes
|
|
||||||
|
> **Note:** Your credentials are stored encrypted per-integration and are never mixed with other users' data. Each user can have multiple OneDrive integrations with independent tokens.
|
||||||
|
|
||||||
|
## Using the System-Level Auth Wizard (Admin)
|
||||||
|
|
||||||
|
The easiest way to configure the global OneDrive integration is to use the built-in auth wizard:
|
||||||
|
|
||||||
|
1. Register an application in Azure AD (see steps below).
|
||||||
|
2. Navigate to the OneDrive Setup page at `/onedrive-setup`.
|
||||||
|
3. Enter your Client ID and other required information.
|
||||||
|
4. Click **Start Authentication Flow**.
|
||||||
|
5. Complete the Microsoft authentication process.
|
||||||
|
6. The system will automatically exchange the authorization code for a refresh token.
|
||||||
|
7. Copy the generated environment variables for your worker nodes.
|
||||||
|
|
||||||
The auth wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment.
|
The auth wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment.
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result Modal -->
|
<!-- Result Modal -->
|
||||||
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
|
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="modalTitle">
|
||||||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
|
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
|
||||||
<div class="mt-3 text-center">
|
<div class="mt-3 text-center">
|
||||||
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
|
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="bg-white shadow-md rounded-lg p-6">
|
<div class="bg-white shadow-md rounded-lg p-6">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
||||||
@@ -13,18 +13,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center my-6">
|
<div class="flex justify-center my-6">
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" role="status" aria-label="Loading"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="processing-message" class="text-center text-gray-700">
|
<div id="processing-message" class="text-center text-gray-700" aria-live="polite">
|
||||||
<p>Exchanging authorization code for refresh token...</p>
|
<p>Exchanging authorization code for refresh token...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="error-container" class="hidden mt-6">
|
<div id="error-container" class="hidden mt-6" role="alert" aria-live="assertive">
|
||||||
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,17 +37,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="/dropbox-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
<a id="return-to-setup-link" href="/dropbox-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
Return to Setup
|
Return to Setup
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="success-container" class="hidden mt-6">
|
<div id="success-container" class="hidden mt-6" aria-live="polite">
|
||||||
<div class="rounded-md bg-green-50 p-4">
|
<div class="rounded-md bg-green-50 p-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,6 +99,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads';
|
const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads';
|
||||||
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
||||||
|
|
||||||
|
// In user mode, point the "Return to Setup" link back to the wizard with integration_id
|
||||||
|
if (integrationId) {
|
||||||
|
const returnLink = document.getElementById('return-to-setup-link');
|
||||||
|
if (returnLink) {
|
||||||
|
returnLink.href = `/dropbox-setup?integration_id=${encodeURIComponent(integrationId)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const redirectUri = window.location.origin + "/dropbox-callback";
|
const redirectUri = window.location.origin + "/dropbox-callback";
|
||||||
|
|
||||||
// Automatically exchange the code for a refresh token
|
// Automatically exchange the code for a refresh token
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result Modal -->
|
<!-- Result Modal -->
|
||||||
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
|
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="modalTitle">
|
||||||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
|
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
|
||||||
<div class="mt-3 text-center">
|
<div class="mt-3 text-center">
|
||||||
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
|
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="bg-white shadow-md rounded-lg p-6">
|
<div class="bg-white shadow-md rounded-lg p-6">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
||||||
@@ -13,18 +13,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center my-6">
|
<div class="flex justify-center my-6">
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" role="status" aria-label="Loading"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="processing-message" class="text-center text-gray-700">
|
<div id="processing-message" class="text-center text-gray-700" aria-live="polite">
|
||||||
<p>Exchanging authorization code for refresh token...</p>
|
<p>Exchanging authorization code for refresh token...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="error-container" class="hidden mt-6">
|
<div id="error-container" class="hidden mt-6" role="alert" aria-live="assertive">
|
||||||
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,17 +37,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="/google-drive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
<a id="return-to-setup-link" href="/google-drive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
Return to Setup
|
Return to Setup
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="folder-selection-container" class="hidden mt-6">
|
<div id="folder-selection-container" class="hidden mt-6" aria-live="polite">
|
||||||
<div class="rounded-md bg-blue-50 p-4 mb-6">
|
<div class="rounded-md bg-blue-50 p-4 mb-6">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<div class="mt-1 flex rounded-md shadow-sm">
|
<div class="mt-1 flex rounded-md shadow-sm">
|
||||||
<input type="text" id="folder-id-input" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" placeholder="Enter Google Drive folder ID">
|
<input type="text" id="folder-id-input" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" placeholder="Enter Google Drive folder ID">
|
||||||
<button id="folder-select-picker-btn" type="button" class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
<button id="folder-select-picker-btn" type="button" class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
<svg class="h-4 w-4 mr-1 text-white-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-4 w-4 mr-1 text-white-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
Select Folder
|
Select Folder
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="success-container" class="hidden mt-6">
|
<div id="success-container" class="hidden mt-6" aria-live="polite">
|
||||||
<div class="rounded-md bg-green-50 p-4">
|
<div class="rounded-md bg-green-50 p-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +140,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const folderId = sessionStorage.getItem('google_drive_folder_id');
|
const folderId = sessionStorage.getItem('google_drive_folder_id');
|
||||||
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
||||||
|
|
||||||
|
// In user mode, point the "Return to Setup" link back to the wizard with integration_id
|
||||||
|
if (integrationId) {
|
||||||
|
const returnLink = document.getElementById('return-to-setup-link');
|
||||||
|
if (returnLink) {
|
||||||
|
returnLink.href = `/google-drive-setup?integration_id=${encodeURIComponent(integrationId)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const redirectUri = window.location.origin + "/google-drive-callback";
|
const redirectUri = window.location.origin + "/google-drive-callback";
|
||||||
|
|
||||||
let accessToken = null;
|
let accessToken = null;
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result Modal -->
|
<!-- Result Modal -->
|
||||||
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
|
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="modalTitle">
|
||||||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
|
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
|
||||||
<div class="mt-3 text-center">
|
<div class="mt-3 text-center">
|
||||||
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
|
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="bg-white shadow-md rounded-lg p-6">
|
<div class="bg-white shadow-md rounded-lg p-6">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
||||||
@@ -13,18 +13,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center my-6">
|
<div class="flex justify-center my-6">
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" role="status" aria-label="Loading"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="processing-message" class="text-center text-gray-700">
|
<div id="processing-message" class="text-center text-gray-700" aria-live="polite">
|
||||||
<p>Exchanging authorization code for refresh token...</p>
|
<p>Exchanging authorization code for refresh token...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="error-container" class="hidden mt-6">
|
<div id="error-container" class="hidden mt-6" role="alert" aria-live="assertive">
|
||||||
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,17 +37,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="/onedrive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
<a id="return-to-setup-link" href="/onedrive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
Return to Setup
|
Return to Setup
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="success-container" class="hidden mt-6">
|
<div id="success-container" class="hidden mt-6" aria-live="polite">
|
||||||
<div class="rounded-md bg-green-50 p-4">
|
<div class="rounded-md bg-green-50 p-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,6 +100,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const folderPath = sessionStorage.getItem('onedrive_folder_path') || "";
|
const folderPath = sessionStorage.getItem('onedrive_folder_path') || "";
|
||||||
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
const integrationId = sessionStorage.getItem('oauth_integration_id');
|
||||||
|
|
||||||
|
// In user mode, point the "Return to Setup" link back to the wizard with integration_id
|
||||||
|
if (integrationId) {
|
||||||
|
const returnLink = document.getElementById('return-to-setup-link');
|
||||||
|
if (returnLink) {
|
||||||
|
returnLink.href = `/onedrive-setup?integration_id=${encodeURIComponent(integrationId)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const redirectUri = window.location.origin + "/onedrive-callback";
|
const redirectUri = window.location.origin + "/onedrive-callback";
|
||||||
|
|
||||||
// Automatically exchange the code for a refresh token
|
// Automatically exchange the code for a refresh token
|
||||||
|
|||||||
@@ -72,7 +72,58 @@ class TestDropboxViews:
|
|||||||
# Should render user mode without errors despite the bad config
|
# Should render user mode without errors despite the bad config
|
||||||
assert b"user_mode" not in response.content or b"Back to Integrations" in response.content
|
assert b"user_mode" not in response.content or b"Back to Integrations" in response.content
|
||||||
|
|
||||||
def test_dropbox_setup_user_mode_valid_config(self, client, db_session):
|
def test_dropbox_setup_user_mode_none_config(self, client, db_session):
|
||||||
|
"""Test user-mode renders correctly when integration.config is None (no folder path)."""
|
||||||
|
owner_id = "user_none_cfg@example.com"
|
||||||
|
integration = UserIntegration(
|
||||||
|
owner_id=owner_id,
|
||||||
|
direction="DESTINATION",
|
||||||
|
integration_type="DROPBOX",
|
||||||
|
name="My Dropbox (no cfg)",
|
||||||
|
config=None,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db_session.add(integration)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(integration)
|
||||||
|
|
||||||
|
with patch("app.views.dropbox.get_current_owner_id", return_value=owner_id):
|
||||||
|
response = client.get(f"/dropbox-setup?integration_id={integration.id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Should render user mode without errors, with empty folder_path
|
||||||
|
assert b"Back to Integrations" in response.content
|
||||||
|
|
||||||
|
def test_dropbox_setup_user_mode_watchfolder_config(self, client, db_session):
|
||||||
|
"""Test user-mode correctly loads folder_path from WATCH_FOLDER source config."""
|
||||||
|
owner_id = "user_wf_cfg@example.com"
|
||||||
|
integration = UserIntegration(
|
||||||
|
owner_id=owner_id,
|
||||||
|
direction="SOURCE",
|
||||||
|
integration_type="WATCH_FOLDER",
|
||||||
|
name="My Dropbox Watch",
|
||||||
|
config=json.dumps({"source_type": "dropbox", "folder_path": "/Inbox"}),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db_session.add(integration)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(integration)
|
||||||
|
|
||||||
|
with patch("app.views.dropbox.get_current_owner_id", return_value=owner_id):
|
||||||
|
response = client.get(f"/dropbox-setup?integration_id={integration.id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"/Inbox" in response.content
|
||||||
|
assert b"Back to Integrations" in response.content
|
||||||
|
|
||||||
|
def test_dropbox_setup_user_mode_integration_not_found(self, client, db_session):
|
||||||
|
"""Test user-mode falls back to admin mode when integration not owned by user."""
|
||||||
|
with patch("app.views.dropbox.get_current_owner_id", return_value="other_user@example.com"):
|
||||||
|
response = client.get("/dropbox-setup?integration_id=999999")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Falls back to admin mode (no "Back to Integrations" link)
|
||||||
|
assert b"Dropbox Integration Setup" in response.content
|
||||||
"""Test user-mode correctly loads folder path from integration config."""
|
"""Test user-mode correctly loads folder path from integration config."""
|
||||||
owner_id = "user_valid_cfg@example.com"
|
owner_id = "user_valid_cfg@example.com"
|
||||||
integration = UserIntegration(
|
integration = UserIntegration(
|
||||||
|
|||||||
@@ -182,6 +182,37 @@ class TestGoogleDriveViews:
|
|||||||
# Should render user mode without errors despite the bad config
|
# Should render user mode without errors despite the bad config
|
||||||
assert b"Back to Integrations" in response.content
|
assert b"Back to Integrations" in response.content
|
||||||
|
|
||||||
|
def test_google_drive_setup_user_mode_none_config(self, client, db_session):
|
||||||
|
"""Test user-mode renders correctly when integration.config is None (no folder ID)."""
|
||||||
|
owner_id = "user_gd_none_cfg@example.com"
|
||||||
|
integration = UserIntegration(
|
||||||
|
owner_id=owner_id,
|
||||||
|
direction="DESTINATION",
|
||||||
|
integration_type="GOOGLE_DRIVE",
|
||||||
|
name="My GDrive (no cfg)",
|
||||||
|
config=None,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db_session.add(integration)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(integration)
|
||||||
|
|
||||||
|
with patch("app.views.google_drive.get_current_owner_id", return_value=owner_id):
|
||||||
|
response = client.get(f"/google-drive-setup?integration_id={integration.id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Should render user mode without errors, with empty folder_id
|
||||||
|
assert b"Back to Integrations" in response.content
|
||||||
|
|
||||||
|
def test_google_drive_setup_user_mode_integration_not_found(self, client, db_session):
|
||||||
|
"""Test user-mode falls back to admin mode when integration not owned by user."""
|
||||||
|
with patch("app.views.google_drive.get_current_owner_id", return_value="other_user@example.com"):
|
||||||
|
response = client.get("/google-drive-setup?integration_id=999999")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Falls back to admin mode (no "Back to Integrations" link)
|
||||||
|
assert b"Google Drive Integration Setup" in response.content
|
||||||
|
|
||||||
def test_google_drive_setup_user_mode_valid_config(self, client, db_session):
|
def test_google_drive_setup_user_mode_valid_config(self, client, db_session):
|
||||||
"""Test user-mode correctly loads folder ID from integration config."""
|
"""Test user-mode correctly loads folder ID from integration config."""
|
||||||
owner_id = "user_gd_valid_cfg@example.com"
|
owner_id = "user_gd_valid_cfg@example.com"
|
||||||
|
|||||||
@@ -70,6 +70,59 @@ class TestOnedriveViews:
|
|||||||
# Should render user mode without errors despite the bad config
|
# Should render user mode without errors despite the bad config
|
||||||
assert b"Back to Integrations" in response.content
|
assert b"Back to Integrations" in response.content
|
||||||
|
|
||||||
|
def test_onedrive_setup_user_mode_none_config(self, client, db_session):
|
||||||
|
"""Test user-mode renders correctly when integration.config is None (no folder path)."""
|
||||||
|
owner_id = "user_od_none_cfg@example.com"
|
||||||
|
integration = UserIntegration(
|
||||||
|
owner_id=owner_id,
|
||||||
|
direction="DESTINATION",
|
||||||
|
integration_type="ONEDRIVE",
|
||||||
|
name="My OneDrive (no cfg)",
|
||||||
|
config=None,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db_session.add(integration)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(integration)
|
||||||
|
|
||||||
|
with patch("app.views.onedrive.get_current_owner_id", return_value=owner_id):
|
||||||
|
response = client.get(f"/onedrive-setup?integration_id={integration.id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Should render user mode without errors, with empty folder_path
|
||||||
|
assert b"Back to Integrations" in response.content
|
||||||
|
|
||||||
|
def test_onedrive_setup_user_mode_watchfolder_config(self, client, db_session):
|
||||||
|
"""Test user-mode correctly loads folder_path from WATCH_FOLDER source config."""
|
||||||
|
owner_id = "user_od_wf_cfg@example.com"
|
||||||
|
integration = UserIntegration(
|
||||||
|
owner_id=owner_id,
|
||||||
|
direction="SOURCE",
|
||||||
|
integration_type="WATCH_FOLDER",
|
||||||
|
name="My OneDrive Watch",
|
||||||
|
config=json.dumps({"source_type": "onedrive", "folder_path": "Work/Inbox"}),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db_session.add(integration)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(integration)
|
||||||
|
|
||||||
|
with patch("app.views.onedrive.get_current_owner_id", return_value=owner_id):
|
||||||
|
response = client.get(f"/onedrive-setup?integration_id={integration.id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Work/Inbox" in response.content
|
||||||
|
assert b"Back to Integrations" in response.content
|
||||||
|
|
||||||
|
def test_onedrive_setup_user_mode_integration_not_found(self, client, db_session):
|
||||||
|
"""Test user-mode falls back to admin mode when integration not owned by user."""
|
||||||
|
with patch("app.views.onedrive.get_current_owner_id", return_value="other_user@example.com"):
|
||||||
|
response = client.get("/onedrive-setup?integration_id=999999")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Falls back to admin mode (no "Back to Integrations" link)
|
||||||
|
assert b"OneDrive Integration Setup" in response.content
|
||||||
|
|
||||||
def test_onedrive_setup_user_mode_valid_config(self, client, db_session):
|
def test_onedrive_setup_user_mode_valid_config(self, client, db_session):
|
||||||
"""Test user-mode correctly loads folder path from integration config."""
|
"""Test user-mode correctly loads folder path from integration config."""
|
||||||
owner_id = "user_od_valid_cfg@example.com"
|
owner_id = "user_od_valid_cfg@example.com"
|
||||||
|
|||||||
Reference in New Issue
Block a user