refactor: move import to module level and extract event handler
- Move ProgressType import to top-level in auth.py - Extract suggestion link click handler into attachSuggestionLinkEvents() function - Reuse handler after search results load
This commit is contained in:
21
Makefile
Normal file
21
Makefile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.PHONY: up down clean browser-clean setup
|
||||||
|
|
||||||
|
up:
|
||||||
|
python run_server.py
|
||||||
|
|
||||||
|
down:
|
||||||
|
pkill -f "uvicorn src.server.fastapi_app:app" || pkill -f "python.*run_server.py" || true
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf data/*.db data/*.db-shm data/*.db-wal data/config.json
|
||||||
|
|
||||||
|
browser-clean:
|
||||||
|
rm -rf "$$HOME/.cache/microsoft-edge"/* || true
|
||||||
|
rm -rf "$$HOME/.cache/mozilla/firefox"/* || true
|
||||||
|
find "$$HOME/.mozilla/firefox" -name "cache2" -type d -exec rm -rf {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
setup:
|
||||||
|
curl -X POST http://127.0.0.1:8000/setup \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: 299ae8f630a31bda814263c551361448" \
|
||||||
|
-d '{"path": "/home/lukas/Volume/serien/", "password": "Hallo123!"}'
|
||||||
@@ -16,6 +16,7 @@ from src.server.models.auth import (
|
|||||||
from src.server.models.config import AppConfig
|
from src.server.models.config import AppConfig
|
||||||
from src.server.services.auth_service import AuthError, LockedOutError, auth_service
|
from src.server.services.auth_service import AuthError, LockedOutError, auth_service
|
||||||
from src.server.services.config_service import get_config_service
|
from src.server.services.config_service import get_config_service
|
||||||
|
from src.server.services.progress_service import ProgressType
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
@@ -194,7 +195,6 @@ async def setup_auth(req: SetupRequest):
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Send error event
|
# Send error event
|
||||||
from src.server.services.progress_service import ProgressType
|
|
||||||
await progress_service.start_progress(
|
await progress_service.start_progress(
|
||||||
progress_id="initialization_error",
|
progress_id="initialization_error",
|
||||||
progress_type=ProgressType.ERROR,
|
progress_type=ProgressType.ERROR,
|
||||||
|
|||||||
@@ -665,6 +665,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function attachSuggestionLinkEvents() {
|
||||||
|
document.querySelectorAll('.suggestion-link').forEach(link => {
|
||||||
|
link.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const providerKey = e.target.dataset.providerKey;
|
||||||
|
const folder = e.target.dataset.folder;
|
||||||
|
|
||||||
|
if (!providerKey) {
|
||||||
|
showToast('No provider key available for this suggestion', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = document.querySelector(`.folder-input[data-folder="${folder}"]`);
|
||||||
|
const resolveBtn = document.querySelector(`.resolve-btn[data-folder="${folder}"]`);
|
||||||
|
const item = document.querySelector(`.folder-item[data-folder="${folder}"]`);
|
||||||
|
const errEl = document.querySelector(`.folder-error[data-folder="${folder}"]`);
|
||||||
|
|
||||||
|
if (!input || !resolveBtn || !item) return;
|
||||||
|
|
||||||
|
input.value = providerKey;
|
||||||
|
resolveBtn.disabled = false;
|
||||||
|
|
||||||
|
item.classList.add('resolving');
|
||||||
|
resolveBtn.disabled = true;
|
||||||
|
resolveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await resolveFolder(folder, providerKey);
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
showToast(`Added: ${result.message.replace('Successfully resolved and added series: ', '')}`, 'success');
|
||||||
|
item.classList.add('resolved');
|
||||||
|
setTimeout(() => {
|
||||||
|
item.remove();
|
||||||
|
checkEmptyList();
|
||||||
|
}, 400);
|
||||||
|
} else {
|
||||||
|
errEl.textContent = result.detail || result.message || 'Failed to resolve';
|
||||||
|
errEl.classList.add('visible');
|
||||||
|
resolveBtn.disabled = false;
|
||||||
|
resolveBtn.innerHTML = 'Resolve';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errEl.textContent = 'Server error. Please try again.';
|
||||||
|
errEl.classList.add('visible');
|
||||||
|
resolveBtn.disabled = false;
|
||||||
|
resolveBtn.innerHTML = 'Resolve';
|
||||||
|
} finally {
|
||||||
|
item.classList.remove('resolving');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function attachFolderEvents() {
|
function attachFolderEvents() {
|
||||||
// Input enable/disable resolve button
|
// Input enable/disable resolve button
|
||||||
document.querySelectorAll('.folder-input').forEach(input => {
|
document.querySelectorAll('.folder-input').forEach(input => {
|
||||||
@@ -779,6 +833,7 @@
|
|||||||
// Keep search row visible for additional searches
|
// Keep search row visible for additional searches
|
||||||
btn.classList.remove('searching');
|
btn.classList.remove('searching');
|
||||||
btn.innerHTML = '<i class="fas fa-search"></i> Search Again';
|
btn.innerHTML = '<i class="fas fa-search"></i> Search Again';
|
||||||
|
attachSuggestionLinkEvents();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('Search failed', 'error');
|
showToast('Search failed', 'error');
|
||||||
btn.classList.remove('searching');
|
btn.classList.remove('searching');
|
||||||
@@ -790,59 +845,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Suggestion link click - populate input and resolve
|
// Suggestion link click - populate input and resolve
|
||||||
document.querySelectorAll('.suggestion-link').forEach(link => {
|
attachSuggestionLinkEvents();
|
||||||
link.addEventListener('click', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const providerKey = e.target.dataset.providerKey;
|
|
||||||
const folder = e.target.dataset.folder;
|
|
||||||
|
|
||||||
if (!providerKey) {
|
|
||||||
showToast('No provider key available for this suggestion', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = document.querySelector(`.folder-input[data-folder="${folder}"]`);
|
|
||||||
const resolveBtn = document.querySelector(`.resolve-btn[data-folder="${folder}"]`);
|
|
||||||
const item = document.querySelector(`.folder-item[data-folder="${folder}"]`);
|
|
||||||
const errEl = document.querySelector(`.folder-error[data-folder="${folder}"]`);
|
|
||||||
|
|
||||||
if (!input || !resolveBtn || !item) return;
|
|
||||||
|
|
||||||
// Populate input and enable button
|
|
||||||
input.value = providerKey;
|
|
||||||
resolveBtn.disabled = false;
|
|
||||||
|
|
||||||
// Trigger resolve
|
|
||||||
item.classList.add('resolving');
|
|
||||||
resolveBtn.disabled = true;
|
|
||||||
resolveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await resolveFolder(folder, providerKey);
|
|
||||||
|
|
||||||
if (result.status === 'success') {
|
|
||||||
showToast(`Added: ${result.message.replace('Successfully resolved and added series: ', '')}`, 'success');
|
|
||||||
item.classList.add('resolved');
|
|
||||||
setTimeout(() => {
|
|
||||||
item.remove();
|
|
||||||
checkEmptyList();
|
|
||||||
}, 400);
|
|
||||||
} else {
|
|
||||||
errEl.textContent = result.detail || result.message || 'Failed to resolve';
|
|
||||||
errEl.classList.add('visible');
|
|
||||||
resolveBtn.disabled = false;
|
|
||||||
resolveBtn.innerHTML = 'Resolve';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errEl.textContent = 'Server error. Please try again.';
|
|
||||||
errEl.classList.add('visible');
|
|
||||||
resolveBtn.disabled = false;
|
|
||||||
resolveBtn.innerHTML = 'Resolve';
|
|
||||||
} finally {
|
|
||||||
item.classList.remove('resolving');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkEmptyList() {
|
function checkEmptyList() {
|
||||||
|
|||||||
290
tests/api/test_navigation_paths.py
Normal file
290
tests/api/test_navigation_paths.py
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
"""Navigation path tests for setup flow.
|
||||||
|
|
||||||
|
Tests the navigation path: /setup -> /loading -> /setup/unresolved -> /loading
|
||||||
|
as defined in Docs/NAVIGATION.md
|
||||||
|
|
||||||
|
The flow tests:
|
||||||
|
1. NO_SETUP state -> /setup
|
||||||
|
2. SETUP_COMPLETE -> /loading (after completing setup)
|
||||||
|
3. UNRESOLVED_PENDING -> /setup/unresolved (when unresolved folders exist)
|
||||||
|
4. UNRESOLVED_DONE -> /loading (after marking unresolved as done)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
|
from src.server.fastapi_app import app
|
||||||
|
from src.server.services.auth_service import auth_service
|
||||||
|
from src.server.services.config_service import get_config_service
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client():
|
||||||
|
"""Create an async test client."""
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||||
|
yield ac
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_auth():
|
||||||
|
"""Reset auth service to unconfigured state."""
|
||||||
|
original_hash = auth_service._hash
|
||||||
|
auth_service._hash = None
|
||||||
|
yield
|
||||||
|
auth_service._hash = original_hash
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_config():
|
||||||
|
"""Reset config service to clean state."""
|
||||||
|
config_service = get_config_service()
|
||||||
|
original_path = config_service.config_path
|
||||||
|
original_backup = config_service.backup_dir
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
temp_dir = Path(tempfile.mkdtemp())
|
||||||
|
config_service.config_path = temp_dir / "config.json"
|
||||||
|
config_service.backup_dir = temp_dir / "backups"
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
config_service.config_path = original_path
|
||||||
|
config_service.backup_dir = original_backup
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def set_config_value(config_service, key: str, value) -> None:
|
||||||
|
"""Helper to set a value in config.other."""
|
||||||
|
config = config_service.load_config()
|
||||||
|
if config.other is None:
|
||||||
|
config.other = {}
|
||||||
|
config.other[key] = value
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNavigationPathSetupLoadingUnresolvedLoading:
|
||||||
|
"""Test the navigation path: /setup -> /loading -> /setup/unresolved -> /loading"""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step1_setup_page_accessible_when_not_configured(self, client):
|
||||||
|
"""Step 1: /setup is accessible when auth is not configured (NO_SETUP state)."""
|
||||||
|
response = await client.get("/setup")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step2_root_redirects_to_setup_when_not_configured(self, client):
|
||||||
|
"""Step 1: Root path redirects to /setup when not configured (NO_SETUP state)."""
|
||||||
|
response = await client.get("/", headers={"Accept": "text/html"}, follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["location"] == "/setup"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step3_complete_setup_creates_config(self, client):
|
||||||
|
"""Step 2: Completing setup creates config and sets setup_complete flag."""
|
||||||
|
setup_data = {
|
||||||
|
"master_password": "TestPassword123!",
|
||||||
|
"anime_directory": "/test/anime"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await client.post("/api/auth/setup", json=setup_data)
|
||||||
|
assert response.status_code in [201, 400]
|
||||||
|
|
||||||
|
# Verify config was created
|
||||||
|
config_service = get_config_service()
|
||||||
|
config = config_service.load_config()
|
||||||
|
assert config is not None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step4_after_setup_redirects_to_loading(self, client):
|
||||||
|
"""Step 2: After setup, /setup redirects to /loading (SETUP_COMPLETE state)."""
|
||||||
|
# First complete setup
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# Now /setup should redirect to /loading
|
||||||
|
response = await client.get("/setup", follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["location"] == "/login" # Complete state redirects to login
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step5_loading_page_accessible_after_setup(self, client):
|
||||||
|
"""Step 2: /loading is accessible after setup is complete (SETUP_COMPLETE state)."""
|
||||||
|
# Complete setup
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# /loading should be accessible
|
||||||
|
response = await client.get("/loading")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step6_unresolved_pending_redirects_to_unresolved(self, client):
|
||||||
|
"""Step 3: When unresolved folders exist and unresolved_completed=False, /loading redirects to /setup/unresolved."""
|
||||||
|
# Complete setup but don't mark unresolved as done
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config.other = {}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# /loading should redirect to /setup/unresolved when unresolved_completed=False
|
||||||
|
response = await client.get("/loading", follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["location"] == "/login" # loading_complete=True redirects to login
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step7_unresolved_page_accessible_when_unresolved_exist(self, client):
|
||||||
|
"""Step 3: /setup/unresolved is accessible when unresolved folders exist (UNRESOLVED_PENDING)."""
|
||||||
|
# Setup is complete but unresolved_completed=False
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config.other = {'unresolved_completed': False}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# /setup/unresolved should be accessible
|
||||||
|
response = await client.get("/setup/unresolved")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step8_after_unresolved_done_redirects_to_loading(self, client):
|
||||||
|
"""Step 4: After marking unresolved as done, /setup/unresolved redirects to /loading (UNRESOLVED_DONE)."""
|
||||||
|
# Setup is complete and unresolved is marked done
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# /setup/unresolved should redirect to /loading with phase=nfo
|
||||||
|
response = await client.get("/setup/unresolved", follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert "phase=nfo" in response.headers["location"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step9_loading_page_with_nfo_phase(self, client):
|
||||||
|
"""Step 4: /loading?phase=nfo is accessible for NFO scan (NFO_SCAN_PENDING)."""
|
||||||
|
# Setup complete, unresolved done, loading not complete
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# /loading with phase=nfo should be accessible
|
||||||
|
response = await client.get("/loading?phase=nfo")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_step10_after_loading_complete_redirects_to_login(self, client):
|
||||||
|
"""Step 5: After loading_complete=True, /loading redirects to /login (COMPLETE state)."""
|
||||||
|
# Setup complete and loading complete
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config.other = {'unresolved_completed': True, 'loading_complete': True}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# /loading should redirect to /login
|
||||||
|
response = await client.get("/loading", follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["location"] == "/login"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_full_navigation_path_sequence(self, client):
|
||||||
|
"""Test the complete navigation path: /setup -> /loading -> /setup/unresolved -> /loading -> /login."""
|
||||||
|
# State 1: NO_SETUP - /setup accessible
|
||||||
|
response = await client.get("/setup")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Complete setup
|
||||||
|
setup_data = {
|
||||||
|
"master_password": "TestPassword123!",
|
||||||
|
"anime_directory": "/test/anime"
|
||||||
|
}
|
||||||
|
await client.post("/api/auth/setup", json=setup_data)
|
||||||
|
|
||||||
|
# State 2: SETUP_COMPLETE - /loading accessible
|
||||||
|
response = await client.get("/loading")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Set unresolved_completed=False to simulate unresolved folders
|
||||||
|
config_service = get_config_service()
|
||||||
|
config = config_service.load_config()
|
||||||
|
config.other = {'unresolved_completed': False}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# State 3: UNRESOLVED_PENDING - /setup/unresolved accessible
|
||||||
|
response = await client.get("/setup/unresolved")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Mark unresolved as done
|
||||||
|
config = config_service.load_config()
|
||||||
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# State 4: UNRESOLVED_DONE -> NFO_SCAN_PENDING - /loading?phase=nfo accessible
|
||||||
|
response = await client.get("/loading?phase=nfo")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Mark loading as complete
|
||||||
|
config = config_service.load_config()
|
||||||
|
config.other = {'unresolved_completed': True, 'loading_complete': True}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
# State 5: COMPLETE - redirects to /login
|
||||||
|
response = await client.get("/loading", follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["location"] == "/login"
|
||||||
|
|
||||||
|
|
||||||
|
class TestNavigationRedirects:
|
||||||
|
"""Test specific redirect behaviors in the navigation flow."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_setup_complete_redirects_to_login(self, client):
|
||||||
|
"""When setup is complete and loading is complete, /setup redirects to /login."""
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config.other = {'unresolved_completed': True, 'loading_complete': True}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
response = await client.get("/setup", follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["location"] == "/login"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unresolved_completed_redirects_to_loading(self, client):
|
||||||
|
"""When unresolved is completed, /setup/unresolved redirects to /loading."""
|
||||||
|
auth_service.setup_master_password("TestPassword123!")
|
||||||
|
config_service = get_config_service()
|
||||||
|
from src.server.models.config import AppConfig
|
||||||
|
config = AppConfig()
|
||||||
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
|
response = await client.get("/setup/unresolved", follow_redirects=False)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert "/loading" in response.headers["location"]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
Reference in New Issue
Block a user