## Copyright © 2026 Olaf Kolkman ## SPDX-License-Identifier: GPL-3.0-or-later import json import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from fastapi.testclient import TestClient from backend.app.main import app client = TestClient(app) def login_headers(username='alice'): token = client.post('/api/auth/login', json={ 'username': username, 'password': 'secret123', }).json()['access_token'] return {'Authorization': f'Bearer {token}'} def test_login_returns_token(): response = client.post('/api/auth/login', json={ 'username': 'alice', 'password': 'secret123', }) assert response.status_code == 200 payload = response.json() assert 'access_token' in payload assert payload['token_type'] == 'bearer' admin_session = client.get('/api/auth/me', params={'token': payload['access_token']}) assert admin_session.status_code == 200 assert admin_session.json()['is_admin'] is True user_token = client.post('/api/auth/login', json={ 'username': 'bob', 'password': 'secret123', }).json()['access_token'] user_session = client.get('/api/auth/me', params={'token': user_token}) assert user_session.status_code == 200 assert user_session.json()['is_admin'] is False def test_configuration_requires_authentication_and_admin_role(): assert client.get('/api/user/me').status_code == 401 assert client.get('/api/admin/plugins').status_code == 401 assert client.get('/api/admin/users').status_code == 401 assert client.get('/api/admin/plugins', headers=login_headers('bob')).status_code == 403 assert client.get('/api/admin/users', headers=login_headers('bob')).status_code == 403 def test_admin_can_add_list_and_remove_users(): headers = login_headers() create_response = client.post('/api/admin/users', headers=headers, json={ 'username': 'charlie', 'email': 'charlie@example.com', 'password': 'charlie-secret', }) assert create_response.status_code == 201 user = create_response.json() assert user['username'] == 'charlie' assert 'password_hash' not in user users = client.get('/api/admin/users', headers=headers).json() assert any(item['id'] == user['id'] for item in users) assert client.delete(f"/api/admin/users/{user['id']}", headers=headers).status_code == 200 assert all(item['id'] != user['id'] for item in client.get('/api/admin/users', headers=headers).json()) assert client.delete('/api/admin/users/user-1', headers=headers).status_code == 400 def test_admin_can_toggle_privileges_without_removing_last_admin(): headers = login_headers() bob = next(user for user in client.get('/api/admin/users', headers=headers).json() if user['username'] == 'bob') promote = client.put(f"/api/admin/users/{bob['id']}", headers=headers, json={'is_admin': True}) assert promote.status_code == 200 assert promote.json()['is_admin'] is True demote = client.put(f"/api/admin/users/{bob['id']}", headers=headers, json={'is_admin': False}) assert demote.status_code == 200 assert demote.json()['is_admin'] is False last_admin = client.put('/api/admin/users/user-1', headers=headers, json={'is_admin': False}) assert last_admin.status_code == 400 def test_users_manage_owned_labels_and_admin_can_delete_any_label(): alice_headers = login_headers('alice') created = client.post('/api/user/labels', headers=alice_headers, json={'name': 'My Label'}) assert created.status_code == 201 label = created.json() assert label['name'] == '#My Label' edited = client.put(f"/api/user/labels/{label['id']}", headers=alice_headers, json={'name': '#Renamed'}) assert edited.status_code == 200 assert edited.json()['name'] == '#Renamed' denied = client.put(f"/api/user/labels/{label['id']}", headers=login_headers('bob'), json={'name': '#Nope'}) assert denied.status_code == 404 assert client.delete(f"/api/user/labels/{label['id']}", headers=login_headers('bob')).status_code == 404 admin_delete = client.delete(f"/api/admin/labels/{label['id']}", headers=alice_headers) assert admin_delete.status_code == 200 def test_labels_page_renders_authenticated_management_shell(): page = client.get('/labels') assert page.status_code == 200 assert 'Labels' in page.text assert 'labels.js?v=1' in page.text def test_submit_link_stores_cleaned_url_and_public_feed(): response = client.post('/api/links', headers=login_headers(), json={ 'title': 'Example page', 'url': 'https://example.com/path?utm_source=ad&utm_medium=email&keep=yes', 'comment': 'Interesting read', 'timestamp': '2026-08-24T12:00:00Z', }) assert response.status_code == 201 data = response.json() assert data['url'] == 'https://example.com/path?keep=yes' feed = client.get('/api/public/feed').json() matching = next(item for item in feed if item['id'] == data['id']) assert matching['comment'] == 'Interesting read' assert matching['user']['username'] == 'alice' assert 'avatar_url' in matching['user'] filtered_response = client.get('/api/public/feed/alice') assert filtered_response.status_code == 200 assert all(item['user']['username'] == 'alice' for item in filtered_response.json()) assert client.get('/api/public/feed/does-not-exist').json() == [] users_response = client.get('/api/public/users') assert users_response.status_code == 200 assert 'alice' in users_response.json() def test_links_support_tags_and_tag_filtering(): headers = login_headers() response = client.post('/api/links', headers=headers, json={ 'title': 'Tagged page', 'url': 'https://example.com/tagged', 'tags': ['#CasePreserved', '#web', 'casepreserved'], }) assert response.status_code == 201 link_id = response.json()['id'] assert response.json()['tags'] == ['#CasePreserved', '#web'] tags = client.get('/api/tags').json() assert any(tag.casefold() == '#casepreserved' for tag in tags) and '#web' in tags assert { '#Internet', '#Cybersecurity', '#Fediverse', '#Food', '#Photography', '#Music', '#AI' } <= set(tags) tagged_feed = client.get('/api/public/feed').json() tagged_item = next(item for item in tagged_feed if item['id'] == link_id) assert {tag.casefold() for tag in tagged_item['tags']} == {'#casepreserved', '#web'} too_many = client.post('/api/links', headers=headers, json={ 'title': 'Too many tags', 'url': 'https://example.com/too-many', 'tags': [f'tag-{index}' for index in range(11)], }) assert too_many.status_code == 422 edited = client.put(f'/api/links/{link_id}', headers=headers, json={ 'title': 'Tagged page', 'url': 'https://example.com/tagged', 'tags': ['edited'], }) assert edited.status_code == 200 assert edited.json()['tags'] == ['#edited'] def test_only_link_owner_can_edit_link(): owner_headers = login_headers('alice') response = client.post('/api/links', headers=owner_headers, json={ 'title': 'Editable link', 'url': 'https://example.com/editable?utm_source=test', 'comment': 'Before edit', }) assert response.status_code == 201 link_id = response.json()['id'] unauthenticated = client.put(f'/api/links/{link_id}', json={ 'title': 'Not allowed', 'url': 'https://example.com/not-allowed', }) assert unauthenticated.status_code == 401 anonymous_feed = client.get('/api/public/feed').json() anonymous_link = next(item for item in anonymous_feed if item['id'] == link_id) assert anonymous_link['is_owner'] is False assert anonymous_link['can_edit'] is False owner_feed = client.get('/api/public/feed', headers=owner_headers).json() owner_link = next(item for item in owner_feed if item['id'] == link_id) assert owner_link['is_owner'] is True assert owner_link['can_edit'] is True edited = client.put(f'/api/links/{link_id}', headers=owner_headers, json={ 'title': 'Edited link', 'url': 'https://example.com/edited', 'comment': 'After edit', }) assert edited.status_code == 200 assert edited.json()['title'] == 'Edited link' denied = client.put(f'/api/links/{link_id}', headers=login_headers('bob'), json={ 'title': 'Not allowed', 'url': 'https://example.com/not-allowed', }) assert denied.status_code == 404 def test_logout_revokes_token_and_admin_can_list_plugins(): headers = login_headers() token = headers['Authorization'].removeprefix('Bearer ') assert client.post('/api/auth/logout', json={'token': token}).status_code == 200 revoked_response = client.post('/api/links', headers=headers, json={ 'title': 'Should fail', 'url': 'https://example.com/path', 'comment': 'nope', }) assert revoked_response.status_code == 401 plugins_response = client.get('/api/admin/plugins', headers=login_headers()) assert plugins_response.status_code == 200 assert any(plugin['name'] == 'default_frontend' for plugin in plugins_response.json()) def test_public_and_admin_pages_render_html(): root_page = client.get('/') assert 'LinkLog' in root_page.text assert 'src="/static/logo.svg"' in root_page.text assert 'class="site-footer"' in root_page.text assert 'https://git.kolkman.org/' in root_page.text assert 'class="menu-toggle"' in root_page.text assert 'id="auth-menu" class="auth-menu hidden"' in root_page.text assert 'id="auth-home-link" href="/">Home' in root_page.text assert '' in root_page.text assert 'id="auth-avatar"' not in root_page.text assert 'id="auth-login-button" href="/login"' in root_page.text assert 'id="auth-profile-link" class="hidden"' in root_page.text assert 'id="auth-admin-link" class="hidden"' in root_page.text assert '