Merge pull request #558 from christianlouis/copilot/fix-user-specific-tokens

feat(auth): make OAuth credentials user-specific via UserIntegration records
This commit is contained in:
Christian Krakau-Louis
2026-03-08 20:23:18 +01:00
committed by GitHub
14 changed files with 282 additions and 15 deletions
+3 -2
View File
@@ -2,7 +2,7 @@
Dropbox integration views for setup and OAuth callback. Dropbox integration views for setup and OAuth callback.
""" """
from fastapi import Request from fastapi import Query, Request
from app.views.base import APIRouter, require_login, settings, templates from app.views.base import APIRouter, require_login, settings, templates
@@ -11,7 +11,7 @@ router = APIRouter()
@router.get("/dropbox-setup") @router.get("/dropbox-setup")
@require_login @require_login
async def dropbox_setup_page(request: Request): async def dropbox_setup_page(request: Request, integration_id: int | None = Query(None)):
""" """
Setup page for the Dropbox integration. Setup page for the Dropbox integration.
Shows configuration status and setup instructions. Shows configuration status and setup instructions.
@@ -28,6 +28,7 @@ async def dropbox_setup_page(request: Request):
"app_secret_value": settings.dropbox_app_secret if settings.dropbox_app_secret else "", "app_secret_value": settings.dropbox_app_secret if settings.dropbox_app_secret else "",
"refresh_token_value": settings.dropbox_refresh_token if settings.dropbox_refresh_token else "", "refresh_token_value": settings.dropbox_refresh_token if settings.dropbox_refresh_token else "",
"folder_path": settings.dropbox_folder or "/Documents/Uploads", # Default folder path "folder_path": settings.dropbox_folder or "/Documents/Uploads", # Default folder path
"integration_id": integration_id,
}, },
) )
+3 -2
View File
@@ -4,7 +4,7 @@ Google Drive integration views for setup and OAuth callback.
import urllib.parse import urllib.parse
from fastapi import Request from fastapi import Query, Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from app.views.base import APIRouter, require_login, settings, templates from app.views.base import APIRouter, require_login, settings, templates
@@ -14,7 +14,7 @@ router = APIRouter()
@router.get("/google-drive-setup") @router.get("/google-drive-setup")
@require_login @require_login
async def google_drive_setup_page(request: Request): async def google_drive_setup_page(request: Request, integration_id: int | None = Query(None)):
""" """
Setup page for the Google Drive integration. Setup page for the Google Drive integration.
Shows configuration status and setup instructions. Shows configuration status and setup instructions.
@@ -55,6 +55,7 @@ async def google_drive_setup_page(request: Request):
"refresh_token_value": settings.google_drive_refresh_token or "", "refresh_token_value": settings.google_drive_refresh_token or "",
"folder_id": settings.google_drive_folder_id or "", "folder_id": settings.google_drive_folder_id or "",
"has_credentials_json": bool(settings.google_drive_credentials_json), "has_credentials_json": bool(settings.google_drive_credentials_json),
"integration_id": integration_id,
}, },
) )
+3 -2
View File
@@ -2,7 +2,7 @@
OneDrive integration views for setup and OAuth callback. OneDrive integration views for setup and OAuth callback.
""" """
from fastapi import Request from fastapi import Query, Request
from app.views.base import APIRouter, require_login, settings, templates from app.views.base import APIRouter, require_login, settings, templates
@@ -11,7 +11,7 @@ router = APIRouter()
@router.get("/onedrive-setup") @router.get("/onedrive-setup")
@require_login @require_login
async def onedrive_setup_page(request: Request): async def onedrive_setup_page(request: Request, integration_id: int | None = Query(None)):
""" """
Setup page for the OneDrive integration. Setup page for the OneDrive integration.
Shows configuration status and setup instructions. Shows configuration status and setup instructions.
@@ -35,6 +35,7 @@ async def onedrive_setup_page(request: Request):
"refresh_token": bool(settings.onedrive_refresh_token), "refresh_token": bool(settings.onedrive_refresh_token),
"refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "", "refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "",
"folder_path": settings.onedrive_folder_path or "Documents/Uploads", # Default folder path "folder_path": settings.onedrive_folder_path or "Documents/Uploads", # Default folder path
"integration_id": integration_id,
}, },
) )
+6
View File
@@ -216,6 +216,12 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
{% block scripts %} {% block scripts %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Store integration_id if provided (for per-user OAuth flow)
const integrationId = "{{ integration_id or '' }}";
if (integrationId) {
sessionStorage.setItem('oauth_integration_id', integrationId);
}
// Elements // Elements
const startAuthFlowBtn = document.getElementById('start-auth-flow'); const startAuthFlowBtn = document.getElementById('start-auth-flow');
const testTokenBtn = document.getElementById('test-token'); const testTokenBtn = document.getElementById('test-token');
+45 -1
View File
@@ -97,6 +97,7 @@ document.addEventListener('DOMContentLoaded', function() {
const appKey = sessionStorage.getItem('dropbox_app_key') || "{{ app_key_value }}"; const appKey = sessionStorage.getItem('dropbox_app_key') || "{{ app_key_value }}";
const appSecret = sessionStorage.getItem('dropbox_app_secret') || "{{ app_secret_value }}"; const appSecret = sessionStorage.getItem('dropbox_app_secret') || "{{ app_secret_value }}";
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 redirectUri = window.location.origin + "/dropbox-callback"; const redirectUri = window.location.origin + "/dropbox-callback";
@@ -137,7 +138,50 @@ document.addEventListener('DOMContentLoaded', function() {
}) })
.then(data => { .then(data => {
if (data.refresh_token) { if (data.refresh_token) {
// Update settings in memory // Per-user flow: save to the user's integration record
if (integrationId) {
const creds = {
refresh_token: data.refresh_token,
app_key: appKey,
app_secret: appSecret,
};
const cfgUpdate = {};
if (folderPath) cfgUpdate.folder = folderPath;
const body = { credentials: creds };
if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate;
document.getElementById('processing-message').innerHTML =
'<p>Saving credentials to your integration...</p>';
return fetch(`/api/integrations/${integrationId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error('Failed to save credentials: ' + (err.detail || 'Unknown error'));
});
}
return response.json();
}).then(() => {
// Clean up
sessionStorage.removeItem('dropbox_app_key');
sessionStorage.removeItem('dropbox_app_secret');
sessionStorage.removeItem('dropbox_folder_path');
sessionStorage.removeItem('oauth_integration_id');
// Show brief success then redirect to integrations
document.getElementById('processing-message').innerHTML =
'<p class="text-green-600 font-semibold">✓ Dropbox authorized successfully!</p>' +
'<p class="text-sm text-gray-500 mt-2">Redirecting to Integrations...</p>';
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
setTimeout(() => { window.location.href = '/integrations'; }, 2000);
});
}
// Global flow: update settings in memory
const updateFormData = new FormData(); const updateFormData = new FormData();
updateFormData.append('refresh_token', data.refresh_token); updateFormData.append('refresh_token', data.refresh_token);
+6
View File
@@ -358,6 +358,12 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
{% block scripts %} {% block scripts %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Store integration_id if provided (for per-user OAuth flow)
const integrationId = "{{ integration_id or '' }}";
if (integrationId) {
sessionStorage.setItem('oauth_integration_id', integrationId);
}
// Tab elements // Tab elements
const oauthTabBtn = document.getElementById('oauth-tab-btn'); const oauthTabBtn = document.getElementById('oauth-tab-btn');
const saTabBtn = document.getElementById('sa-tab-btn'); const saTabBtn = document.getElementById('sa-tab-btn');
+46 -1
View File
@@ -138,6 +138,7 @@ document.addEventListener('DOMContentLoaded', function() {
const clientId = sessionStorage.getItem('google_drive_client_id'); const clientId = sessionStorage.getItem('google_drive_client_id');
const clientSecret = sessionStorage.getItem('google_drive_client_secret'); const clientSecret = sessionStorage.getItem('google_drive_client_secret');
const folderId = sessionStorage.getItem('google_drive_folder_id'); const folderId = sessionStorage.getItem('google_drive_folder_id');
const integrationId = sessionStorage.getItem('oauth_integration_id');
const redirectUri = window.location.origin + "/google-drive-callback"; const redirectUri = window.location.origin + "/google-drive-callback";
@@ -192,7 +193,51 @@ document.addEventListener('DOMContentLoaded', function() {
refreshToken = data.refresh_token; refreshToken = data.refresh_token;
accessToken = data.access_token; accessToken = data.access_token;
// Update settings in memory first // Per-user flow: save to the user's integration record
if (integrationId) {
const creds = {
client_id: clientId,
client_secret: clientSecret,
refresh_token: data.refresh_token,
};
const cfgUpdate = {};
if (folderId) cfgUpdate.folder_id = folderId;
const body = { credentials: creds };
if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate;
document.getElementById('processing-message').innerHTML =
'<p>Saving credentials to your integration...</p>';
return fetch(`/api/integrations/${integrationId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error('Failed to save credentials: ' + (err.detail || 'Unknown error'));
});
}
return response.json();
}).then(() => {
// Clean up
sessionStorage.removeItem('google_drive_client_id');
sessionStorage.removeItem('google_drive_client_secret');
sessionStorage.removeItem('google_drive_folder_id');
sessionStorage.removeItem('google_drive_use_oauth');
sessionStorage.removeItem('oauth_integration_id');
// Show brief success then redirect to integrations
document.getElementById('processing-message').innerHTML =
'<p class="text-green-600 font-semibold">✓ Google Drive authorized successfully!</p>' +
'<p class="text-sm text-gray-500 mt-2">Redirecting to Integrations...</p>';
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
setTimeout(() => { window.location.href = '/integrations'; }, 2000);
});
}
// Global flow: update settings in memory first
const updateFormData = new FormData(); const updateFormData = new FormData();
updateFormData.append('refresh_token', data.refresh_token); updateFormData.append('refresh_token', data.refresh_token);
+27 -6
View File
@@ -206,6 +206,16 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
<template x-if="isOAuthType(intg.integration_type) && !intg.has_credentials">
<a
:href="oauthLink(intg.integration_type) + '?integration_id=' + intg.id"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-yellow-400 text-yellow-700 bg-yellow-50 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-yellow-500"
style="min-height:36px;"
:aria-label="`Authorize ${intg.name}`"
>
<i class="fas fa-key mr-1" aria-hidden="true"></i>Authorize
</a>
</template>
<button <button
type="button" type="button"
@click="testSavedIntegration(intg)" @click="testSavedIntegration(intg)"
@@ -279,6 +289,16 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
<template x-if="isOAuthType(intg.integration_type) && !intg.has_credentials">
<a
:href="oauthLink(intg.integration_type) + '?integration_id=' + intg.id"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded border border-yellow-400 text-yellow-700 bg-yellow-50 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-yellow-500"
style="min-height:36px;"
:aria-label="`Authorize ${intg.name}`"
>
<i class="fas fa-key mr-1" aria-hidden="true"></i>Authorize
</a>
</template>
<button <button
type="button" type="button"
@click="testSavedIntegration(intg)" @click="testSavedIntegration(intg)"
@@ -561,9 +581,8 @@
</div> </div>
<div class="bg-blue-50 dark:bg-blue-900/30 rounded-md p-3 text-sm text-blue-700 dark:text-blue-300"> <div class="bg-blue-50 dark:bg-blue-900/30 rounded-md p-3 text-sm text-blue-700 dark:text-blue-300">
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> <i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
OAuth tokens can be configured via the dedicated Save this integration first, then use the <strong>Authorize</strong> button to complete the OAuth flow.
<a :href="oauthLink(form.integration_type)" class="underline hover:text-blue-900 dark:hover:text-blue-100" x-text="typeLabel(form.integration_type) + ' setup page'"></a>. Your credentials will be stored securely in your personal integration record.
Once authorised, the token is stored automatically.
</div> </div>
</div> </div>
</template> </template>
@@ -742,6 +761,7 @@ function integrationsDashboard() {
const SOURCE_TYPES = ['IMAP', 'WATCH_FOLDER', 'WEBHOOK']; const SOURCE_TYPES = ['IMAP', 'WATCH_FOLDER', 'WEBHOOK'];
const DEST_TYPES = ['S3', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'EMAIL', 'PAPERLESS', 'RCLONE']; const DEST_TYPES = ['S3', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'EMAIL', 'PAPERLESS', 'RCLONE'];
const TYPES_WITH_FORM_FIELDS = new Set(['IMAP', 'S3', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'EMAIL', 'WATCH_FOLDER', 'PAPERLESS']); const TYPES_WITH_FORM_FIELDS = new Set(['IMAP', 'S3', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'EMAIL', 'WATCH_FOLDER', 'PAPERLESS']);
const OAUTH_TYPES = new Set(['DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE']);
const TYPE_LABELS = { const TYPE_LABELS = {
IMAP: 'IMAP Email', IMAP: 'IMAP Email',
@@ -838,11 +858,12 @@ function integrationsDashboard() {
typeLabel(t) { return TYPE_LABELS[t] || t; }, typeLabel(t) { return TYPE_LABELS[t] || t; },
typeIcon(t) { return TYPE_ICONS[t] || 'fa-plug text-gray-400'; }, typeIcon(t) { return TYPE_ICONS[t] || 'fa-plug text-gray-400'; },
hasFormFields(t) { return TYPES_WITH_FORM_FIELDS.has(t); }, hasFormFields(t) { return TYPES_WITH_FORM_FIELDS.has(t); },
isOAuthType(t) { return OAUTH_TYPES.has(t); },
oauthLink(t) { oauthLink(t) {
if (t === 'DROPBOX') return '/dropbox'; if (t === 'DROPBOX') return '/dropbox-setup';
if (t === 'GOOGLE_DRIVE') return '/google-drive'; if (t === 'GOOGLE_DRIVE') return '/google-drive-setup';
if (t === 'ONEDRIVE') return '/onedrive'; if (t === 'ONEDRIVE') return '/onedrive-setup';
return '#'; return '#';
}, },
+6
View File
@@ -237,6 +237,12 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
{% block scripts %} {% block scripts %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Store integration_id if provided (for per-user OAuth flow)
const integrationId = "{{ integration_id or '' }}";
if (integrationId) {
sessionStorage.setItem('oauth_integration_id', integrationId);
}
// Elements // Elements
const startAuthFlowBtn = document.getElementById('start-auth-flow'); const startAuthFlowBtn = document.getElementById('start-auth-flow');
const testTokenBtn = document.getElementById('test-token'); const testTokenBtn = document.getElementById('test-token');
+47 -1
View File
@@ -98,6 +98,7 @@ document.addEventListener('DOMContentLoaded', function() {
const clientSecret = sessionStorage.getItem('onedrive_client_secret') || "{{ client_secret_value }}"; const clientSecret = sessionStorage.getItem('onedrive_client_secret') || "{{ client_secret_value }}";
const tenantId = sessionStorage.getItem('onedrive_tenant_id') || "{{ tenant_id }}" || "common"; const tenantId = sessionStorage.getItem('onedrive_tenant_id') || "{{ tenant_id }}" || "common";
const folderPath = sessionStorage.getItem('onedrive_folder_path') || ""; const folderPath = sessionStorage.getItem('onedrive_folder_path') || "";
const integrationId = sessionStorage.getItem('oauth_integration_id');
const redirectUri = window.location.origin + "/onedrive-callback"; const redirectUri = window.location.origin + "/onedrive-callback";
@@ -140,7 +141,52 @@ document.addEventListener('DOMContentLoaded', function() {
}) })
.then(data => { .then(data => {
if (data.refresh_token) { if (data.refresh_token) {
// Instead of saving to .env file, update settings in memory // Per-user flow: save to the user's integration record
if (integrationId) {
const creds = {
client_id: clientId,
client_secret: clientSecret,
refresh_token: data.refresh_token,
tenant_id: tenantId,
};
const cfgUpdate = {};
if (folderPath) cfgUpdate.folder_path = folderPath;
const body = { credentials: creds };
if (Object.keys(cfgUpdate).length > 0) body.config = cfgUpdate;
document.getElementById('processing-message').innerHTML =
'<p>Saving credentials to your integration...</p>';
return fetch(`/api/integrations/${integrationId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error('Failed to save credentials: ' + (err.detail || 'Unknown error'));
});
}
return response.json();
}).then(() => {
// Clean up
sessionStorage.removeItem('onedrive_client_id');
sessionStorage.removeItem('onedrive_client_secret');
sessionStorage.removeItem('onedrive_tenant_id');
sessionStorage.removeItem('onedrive_folder_path');
sessionStorage.removeItem('oauth_integration_id');
// Show brief success then redirect to integrations
document.getElementById('processing-message').innerHTML =
'<p class="text-green-600 font-semibold">✓ OneDrive authorized successfully!</p>' +
'<p class="text-sm text-gray-500 mt-2">Redirecting to Integrations...</p>';
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
setTimeout(() => { window.location.href = '/integrations'; }, 2000);
});
}
// Global flow: update settings in memory
const updateFormData = new FormData(); const updateFormData = new FormData();
updateFormData.append('refresh_token', data.refresh_token); updateFormData.append('refresh_token', data.refresh_token);
+18
View File
@@ -26,3 +26,21 @@ class TestDropboxViews:
"""Test the Dropbox OAuth callback with auth code.""" """Test the Dropbox OAuth callback with auth code."""
response = client.get("/dropbox-callback?code=test_code") response = client.get("/dropbox-callback?code=test_code")
assert response.status_code == 200 assert response.status_code == 200
def test_dropbox_setup_page_with_integration_id(self, client):
"""Test the Dropbox setup page accepts integration_id query param."""
response = client.get("/dropbox-setup?integration_id=42")
assert response.status_code == 200
# The template should store the integration_id for per-user OAuth flow
assert b"oauth_integration_id" in response.content
assert b"42" in response.content
def test_dropbox_setup_page_without_integration_id(self, client):
"""Test the Dropbox setup page works without integration_id (global flow)."""
response = client.get("/dropbox-setup")
assert response.status_code == 200
# The template should not set integration_id when not provided
body = response.text
assert "oauth_integration_id" in body # The JS code is always present
# But integration_id template var should be empty
assert 'const integrationId = ""' in body
+16
View File
@@ -30,6 +30,22 @@ class TestGoogleDriveViews:
response = client.get("/google-drive-callback?code=test_code") response = client.get("/google-drive-callback?code=test_code")
assert response.status_code == 200 assert response.status_code == 200
def test_google_drive_setup_page_with_integration_id(self, client):
"""Test the Google Drive setup page accepts integration_id query param."""
response = client.get("/google-drive-setup?integration_id=99")
assert response.status_code == 200
# The template should store the integration_id for per-user OAuth flow
assert b"oauth_integration_id" in response.content
assert b"99" in response.content
def test_google_drive_setup_page_without_integration_id(self, client):
"""Test the Google Drive setup page works without integration_id (global flow)."""
response = client.get("/google-drive-setup")
assert response.status_code == 200
body = response.text
assert "oauth_integration_id" in body
assert 'const integrationId = ""' in body
def test_google_drive_callback_with_code_and_state(self, client): def test_google_drive_callback_with_code_and_state(self, client):
"""Test the Google Drive OAuth callback with code and state.""" """Test the Google Drive OAuth callback with code and state."""
response = client.get("/google-drive-callback?code=test_code&state=test_state") response = client.get("/google-drive-callback?code=test_code&state=test_state")
+40
View File
@@ -63,6 +63,46 @@ class TestIntegrationsDashboardView:
assert response.status_code == 200 assert response.status_code == 200
assert b"integrationsDashboard" in response.content assert b"integrationsDashboard" in response.content
def test_integrations_page_oauth_links_point_to_setup_pages(self, client):
"""oauthLink() must reference the correct -setup URLs, not bare paths."""
response = client.get("/integrations")
assert response.status_code == 200
body = response.text
assert "'/dropbox-setup'" in body or '"/dropbox-setup"' in body
assert "'/google-drive-setup'" in body or '"/google-drive-setup"' in body
assert "'/onedrive-setup'" in body or '"/onedrive-setup"' in body
# Ensure the old broken paths are gone
assert "return '/dropbox'" not in body.replace("/dropbox-setup", "")
assert "return '/google-drive'" not in body.replace("/google-drive-setup", "")
assert "return '/onedrive'" not in body.replace("/onedrive-setup", "")
def test_integrations_page_contains_authorize_button(self, client):
"""GET /integrations page includes per-user Authorize button for OAuth types."""
response = client.get("/integrations")
assert response.status_code == 200
body = response.text
assert "isOAuthType" in body
assert "Authorize" in body
assert "integration_id=" in body
def test_integrations_page_contains_oauth_types_constant(self, client):
"""GET /integrations includes OAUTH_TYPES Set constant."""
response = client.get("/integrations")
assert response.status_code == 200
body = response.text
assert "OAUTH_TYPES" in body
assert "'DROPBOX'" in body
assert "'GOOGLE_DRIVE'" in body
assert "'ONEDRIVE'" in body
def test_integrations_page_modal_info_box_per_user(self, client):
"""The create modal info box instructs to save first then authorize."""
response = client.get("/integrations")
assert response.status_code == 200
body = response.text
assert "Save this integration first" in body
assert "personal integration record" in body
def test_integrations_page_contains_quota_indicators(self, client): def test_integrations_page_contains_quota_indicators(self, client):
"""GET /integrations page includes quota labels.""" """GET /integrations page includes quota labels."""
response = client.get("/integrations") response = client.get("/integrations")
+16
View File
@@ -26,3 +26,19 @@ class TestOnedriveViews:
"""Test the OneDrive OAuth callback with auth code.""" """Test the OneDrive OAuth callback with auth code."""
response = client.get("/onedrive-callback?code=test_code") response = client.get("/onedrive-callback?code=test_code")
assert response.status_code == 200 assert response.status_code == 200
def test_onedrive_setup_page_with_integration_id(self, client):
"""Test the OneDrive setup page accepts integration_id query param."""
response = client.get("/onedrive-setup?integration_id=77")
assert response.status_code == 200
# The template should store the integration_id for per-user OAuth flow
assert b"oauth_integration_id" in response.content
assert b"77" in response.content
def test_onedrive_setup_page_without_integration_id(self, client):
"""Test the OneDrive setup page works without integration_id (global flow)."""
response = client.get("/onedrive-setup")
assert response.status_code == 200
body = response.text
assert "oauth_integration_id" in body
assert 'const integrationId = ""' in body