7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
443 lines
16 KiB
Python
443 lines
16 KiB
Python
"""
|
|
Tests for Collection CRUD API endpoints.
|
|
|
|
Tests the following endpoints:
|
|
- GET /library/api/collections
|
|
- POST /library/api/collections
|
|
- PUT /library/api/collections/<id>
|
|
- DELETE /library/api/collections/<id>
|
|
"""
|
|
|
|
import uuid
|
|
|
|
|
|
# Import helpers from conftest
|
|
from .conftest import delete_test_collection
|
|
|
|
|
|
class TestGetCollections:
|
|
"""Tests for GET /library/api/collections endpoint."""
|
|
|
|
def test_get_collections_unauthenticated(self, client):
|
|
"""Unauthenticated request is rejected."""
|
|
response = client.get("/library/api/collections")
|
|
# Should redirect to login or return 401
|
|
assert response.status_code in [302, 401]
|
|
|
|
def test_get_collections_authenticated_empty(self, authenticated_client):
|
|
"""Returns collections list (may include default Library)."""
|
|
response = authenticated_client.get("/library/api/collections")
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is True
|
|
assert "collections" in data
|
|
assert isinstance(data["collections"], list)
|
|
|
|
def test_get_collections_with_data(
|
|
self, authenticated_client, create_collection_helper
|
|
):
|
|
"""Returns created collections."""
|
|
# Create a test collection
|
|
created = create_collection_helper(
|
|
name="API Test Collection",
|
|
description="Testing GET endpoint",
|
|
)
|
|
assert created is not None
|
|
|
|
response = authenticated_client.get("/library/api/collections")
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is True
|
|
|
|
# Find our created collection
|
|
collection_names = [c["name"] for c in data["collections"]]
|
|
assert "API Test Collection" in collection_names
|
|
|
|
def test_get_collections_includes_metadata(
|
|
self, authenticated_client, create_collection_helper
|
|
):
|
|
"""Collections include required metadata fields."""
|
|
created = create_collection_helper()
|
|
assert created is not None
|
|
|
|
response = authenticated_client.get("/library/api/collections")
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert len(data["collections"]) > 0
|
|
|
|
# Check metadata fields
|
|
coll = data["collections"][0]
|
|
assert "id" in coll
|
|
assert "name" in coll
|
|
assert "description" in coll
|
|
assert "created_at" in coll
|
|
assert "collection_type" in coll
|
|
assert "document_count" in coll
|
|
|
|
|
|
class TestCreateCollection:
|
|
"""Tests for POST /library/api/collections endpoint."""
|
|
|
|
def test_create_collection_unauthenticated(self, client):
|
|
"""Unauthenticated request is rejected."""
|
|
response = client.post(
|
|
"/library/api/collections",
|
|
json={"name": "Test", "description": "Test"},
|
|
content_type="application/json",
|
|
)
|
|
assert response.status_code in [302, 401]
|
|
|
|
def test_create_collection_success(self, authenticated_client):
|
|
"""Successfully creates a new collection."""
|
|
unique_name = f"Test Collection {uuid.uuid4().hex[:8]}"
|
|
|
|
response = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={
|
|
"name": unique_name,
|
|
"description": "A test collection",
|
|
"type": "user_uploads",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is True
|
|
assert data["collection"]["name"] == unique_name
|
|
assert data["collection"]["description"] == "A test collection"
|
|
assert "id" in data["collection"]
|
|
|
|
# Cleanup
|
|
delete_test_collection(authenticated_client, data["collection"]["id"])
|
|
|
|
def test_create_collection_defaults_private(self, authenticated_client):
|
|
"""A new collection with no is_public flag defaults to private."""
|
|
name = f"Priv {uuid.uuid4().hex[:8]}"
|
|
resp = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": name},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
coll = resp.get_json()["collection"]
|
|
assert coll["is_public"] is False
|
|
delete_test_collection(authenticated_client, coll["id"])
|
|
|
|
def test_create_and_toggle_collection_is_public(self, authenticated_client):
|
|
"""is_public can be set at create and flipped via PUT, and is
|
|
reflected in the list API."""
|
|
name = f"Pub {uuid.uuid4().hex[:8]}"
|
|
resp = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": name, "is_public": True},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
coll = resp.get_json()["collection"]
|
|
cid = coll["id"]
|
|
assert coll["is_public"] is True
|
|
|
|
# List reflects it
|
|
listing = authenticated_client.get(
|
|
"/library/api/collections"
|
|
).get_json()["collections"]
|
|
match = next(c for c in listing if c["id"] == cid)
|
|
assert match["is_public"] is True
|
|
|
|
# The collection-details load path (documents endpoint) also exposes
|
|
# is_public so the details-page toggle can reflect current state.
|
|
docs = authenticated_client.get(
|
|
f"/library/api/collections/{cid}/documents"
|
|
)
|
|
assert docs.status_code == 200
|
|
assert docs.get_json()["collection"]["is_public"] is True
|
|
|
|
# Flip back to private via PUT
|
|
put = authenticated_client.put(
|
|
f"/library/api/collections/{cid}",
|
|
json={"is_public": False},
|
|
content_type="application/json",
|
|
)
|
|
assert put.status_code == 200
|
|
assert put.get_json()["collection"]["is_public"] is False
|
|
|
|
delete_test_collection(authenticated_client, cid)
|
|
|
|
def test_create_collection_empty_name(self, authenticated_client):
|
|
"""Rejects collection with empty name."""
|
|
response = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": "", "description": "No name"},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is False
|
|
assert (
|
|
"required" in data["error"].lower()
|
|
or "name" in data["error"].lower()
|
|
)
|
|
|
|
def test_create_collection_whitespace_name(self, authenticated_client):
|
|
"""Rejects collection with whitespace-only name."""
|
|
response = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": " ", "description": "Whitespace name"},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
def test_create_collection_duplicate_name(self, authenticated_client):
|
|
"""Rejects collection with duplicate name."""
|
|
unique_name = f"Duplicate Test {uuid.uuid4().hex[:8]}"
|
|
|
|
# Create first collection
|
|
response1 = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": unique_name, "description": "First"},
|
|
content_type="application/json",
|
|
)
|
|
assert response1.status_code == 200
|
|
collection_id = response1.get_json()["collection"]["id"]
|
|
|
|
try:
|
|
# Try to create duplicate
|
|
response2 = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": unique_name, "description": "Duplicate"},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response2.status_code == 400
|
|
|
|
data = response2.get_json()
|
|
assert data["success"] is False
|
|
assert "exists" in data["error"].lower()
|
|
finally:
|
|
# Cleanup
|
|
delete_test_collection(authenticated_client, collection_id)
|
|
|
|
def test_create_collection_without_description(self, authenticated_client):
|
|
"""Creates collection without description."""
|
|
unique_name = f"No Desc Collection {uuid.uuid4().hex[:8]}"
|
|
|
|
response = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": unique_name},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is True
|
|
assert data["collection"]["name"] == unique_name
|
|
|
|
# Cleanup
|
|
delete_test_collection(authenticated_client, data["collection"]["id"])
|
|
|
|
|
|
class TestUpdateCollection:
|
|
"""Tests for PUT /library/api/collections/<id> endpoint."""
|
|
|
|
def test_update_collection_unauthenticated(self, client):
|
|
"""Unauthenticated request is rejected."""
|
|
response = client.put(
|
|
"/library/api/collections/some-id",
|
|
json={"name": "Updated"},
|
|
content_type="application/json",
|
|
)
|
|
assert response.status_code in [302, 401]
|
|
|
|
def test_update_collection_name(
|
|
self, authenticated_client, create_collection_helper
|
|
):
|
|
"""Successfully updates collection name."""
|
|
created = create_collection_helper(name="Original Name")
|
|
assert created is not None
|
|
|
|
new_name = f"Updated Name {uuid.uuid4().hex[:8]}"
|
|
response = authenticated_client.put(
|
|
f"/library/api/collections/{created['id']}",
|
|
json={"name": new_name},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is True
|
|
assert data["collection"]["name"] == new_name
|
|
|
|
def test_update_collection_description(
|
|
self, authenticated_client, create_collection_helper
|
|
):
|
|
"""Successfully updates collection description."""
|
|
created = create_collection_helper(description="Original description")
|
|
assert created is not None
|
|
|
|
response = authenticated_client.put(
|
|
f"/library/api/collections/{created['id']}",
|
|
json={"description": "Updated description"},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is True
|
|
assert data["collection"]["description"] == "Updated description"
|
|
|
|
def test_update_collection_not_found(self, authenticated_client):
|
|
"""Returns 404 for non-existent collection."""
|
|
fake_id = str(uuid.uuid4())
|
|
response = authenticated_client.put(
|
|
f"/library/api/collections/{fake_id}",
|
|
json={"name": "Updated"},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is False
|
|
assert "not found" in data["error"].lower()
|
|
|
|
def test_update_collection_duplicate_name(
|
|
self, authenticated_client, create_collection_helper
|
|
):
|
|
"""Rejects update with duplicate name."""
|
|
# Create two collections
|
|
coll1 = create_collection_helper(
|
|
name=f"Collection A {uuid.uuid4().hex[:8]}"
|
|
)
|
|
coll2 = create_collection_helper(
|
|
name=f"Collection B {uuid.uuid4().hex[:8]}"
|
|
)
|
|
assert coll1 is not None and coll2 is not None
|
|
|
|
# Try to rename coll2 to coll1's name
|
|
response = authenticated_client.put(
|
|
f"/library/api/collections/{coll2['id']}",
|
|
json={"name": coll1["name"]},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is False
|
|
assert "exists" in data["error"].lower()
|
|
|
|
|
|
class TestDeleteCollection:
|
|
"""Tests for DELETE /library/api/collections/<id> endpoint."""
|
|
|
|
def test_delete_collection_unauthenticated(self, client):
|
|
"""Unauthenticated request is rejected."""
|
|
response = client.delete("/library/api/collections/some-id")
|
|
assert response.status_code in [302, 401]
|
|
|
|
def test_delete_collection_success(self, authenticated_client):
|
|
"""Successfully deletes a collection."""
|
|
# Create a collection to delete
|
|
unique_name = f"To Delete {uuid.uuid4().hex[:8]}"
|
|
create_response = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": unique_name, "description": "Will be deleted"},
|
|
content_type="application/json",
|
|
)
|
|
assert create_response.status_code == 200
|
|
collection_id = create_response.get_json()["collection"]["id"]
|
|
|
|
# Delete it
|
|
response = authenticated_client.delete(
|
|
f"/library/api/collections/{collection_id}"
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is True
|
|
|
|
# Verify it's gone
|
|
list_response = authenticated_client.get("/library/api/collections")
|
|
collections = list_response.get_json()["collections"]
|
|
collection_ids = [c["id"] for c in collections]
|
|
assert collection_id not in collection_ids
|
|
|
|
def test_delete_collection_not_found(self, authenticated_client):
|
|
"""Returns 404 for non-existent collection."""
|
|
fake_id = str(uuid.uuid4())
|
|
response = authenticated_client.delete(
|
|
f"/library/api/collections/{fake_id}"
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
data = response.get_json()
|
|
assert data["success"] is False
|
|
assert "not found" in data["error"].lower()
|
|
|
|
|
|
class TestCollectionIntegration:
|
|
"""Integration tests for collection workflow."""
|
|
|
|
def test_create_read_update_delete_flow(self, authenticated_client):
|
|
"""Full CRUD workflow."""
|
|
unique_name = f"CRUD Test {uuid.uuid4().hex[:8]}"
|
|
|
|
# Create
|
|
create_response = authenticated_client.post(
|
|
"/library/api/collections",
|
|
json={"name": unique_name, "description": "Initial"},
|
|
content_type="application/json",
|
|
)
|
|
assert create_response.status_code == 200
|
|
collection_id = create_response.get_json()["collection"]["id"]
|
|
|
|
try:
|
|
# Read
|
|
list_response = authenticated_client.get("/library/api/collections")
|
|
assert list_response.status_code == 200
|
|
collections = list_response.get_json()["collections"]
|
|
our_coll = next(
|
|
(c for c in collections if c["id"] == collection_id), None
|
|
)
|
|
assert our_coll is not None
|
|
assert our_coll["name"] == unique_name
|
|
|
|
# Update
|
|
updated_name = f"Updated {uuid.uuid4().hex[:8]}"
|
|
update_response = authenticated_client.put(
|
|
f"/library/api/collections/{collection_id}",
|
|
json={"name": updated_name, "description": "Updated desc"},
|
|
content_type="application/json",
|
|
)
|
|
assert update_response.status_code == 200
|
|
|
|
# Verify update
|
|
list_response2 = authenticated_client.get(
|
|
"/library/api/collections"
|
|
)
|
|
collections2 = list_response2.get_json()["collections"]
|
|
our_coll2 = next(
|
|
(c for c in collections2 if c["id"] == collection_id), None
|
|
)
|
|
assert our_coll2["name"] == updated_name
|
|
assert our_coll2["description"] == "Updated desc"
|
|
|
|
finally:
|
|
# Delete
|
|
delete_response = authenticated_client.delete(
|
|
f"/library/api/collections/{collection_id}"
|
|
)
|
|
assert delete_response.status_code == 200
|