fed8b2eed7
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
603 lines
24 KiB
Python
603 lines
24 KiB
Python
"""Tests for the wiki source routes in application/api/user/sources/routes.py."""
|
|
|
|
import uuid
|
|
from contextlib import contextmanager
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
return Flask(__name__)
|
|
|
|
|
|
@contextmanager
|
|
def _patch_db(conn):
|
|
@contextmanager
|
|
def _yield():
|
|
yield conn
|
|
|
|
with patch(
|
|
"application.api.user.sources.routes.db_session", _yield
|
|
), patch(
|
|
"application.api.user.sources.routes.db_readonly", _yield
|
|
):
|
|
yield
|
|
|
|
|
|
def _grant_team_access(pg_conn, owner, member, source_id, access_level):
|
|
from application.storage.db.repositories.team_members import (
|
|
TeamMembersRepository,
|
|
)
|
|
from application.storage.db.repositories.team_resource_grants import (
|
|
TeamResourceGrantsRepository,
|
|
)
|
|
from application.storage.db.repositories.teams import TeamsRepository
|
|
|
|
team = TeamsRepository(pg_conn).create(
|
|
"Acme", f"acme-{uuid.uuid4().hex[:8]}", owner
|
|
)
|
|
TeamMembersRepository(pg_conn).add_member(
|
|
team["id"], member, role="team_member"
|
|
)
|
|
TeamResourceGrantsRepository(pg_conn).grant(
|
|
team["id"], "source", source_id, owner_id=owner, granted_by=owner,
|
|
access_level=access_level,
|
|
)
|
|
|
|
|
|
class TestCreateWikiSource:
|
|
def test_returns_401_unauthenticated(self, app):
|
|
from application.api.user.sources.routes import CreateWikiSource
|
|
|
|
with app.test_request_context(
|
|
"/api/sources/wiki", method="POST", json={"name": "w"}
|
|
):
|
|
from flask import request
|
|
request.decoded_token = None
|
|
response = CreateWikiSource().post()
|
|
assert response.status_code == 401
|
|
|
|
def test_returns_400_missing_name(self, app, pg_conn):
|
|
from application.api.user.sources.routes import CreateWikiSource
|
|
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
"/api/sources/wiki", method="POST", json={}
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": "u-wiki"}
|
|
response = CreateWikiSource().post()
|
|
assert response.status_code == 400
|
|
|
|
def test_creates_row_without_ingest(self, app, pg_conn):
|
|
from application.api.user.sources.routes import CreateWikiSource
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-wiki-create"
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.reembed_wiki_page.delay"
|
|
) as mock_reembed, patch(
|
|
"application.api.user.tasks.ingest.delay"
|
|
) as mock_ingest, patch(
|
|
"application.api.user.tasks.reingest_source_task.delay"
|
|
) as mock_reingest, app.test_request_context(
|
|
"/api/sources/wiki", method="POST", json={"name": "My Wiki"}
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = CreateWikiSource().post()
|
|
|
|
assert response.status_code == 200
|
|
source_id = response.json["source_id"]
|
|
row = SourcesRepository(pg_conn).get_any(source_id, user)
|
|
assert row["type"] == "wiki"
|
|
assert row["config"]["kind"] == "wiki"
|
|
assert int(row["tokens"]) == 0
|
|
# No seed content → no re-embed, and never any ingest/reingest task.
|
|
mock_reembed.assert_not_called()
|
|
mock_ingest.assert_not_called()
|
|
mock_reingest.assert_not_called()
|
|
|
|
def test_seed_page_roundtrips_and_only_seed_reembeds(self, app, pg_conn):
|
|
from application.api.user.sources.routes import (
|
|
CreateWikiSource,
|
|
WikiPage,
|
|
WIKI_INDEX_PATH,
|
|
)
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-wiki-seed"
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.reembed_wiki_page.delay"
|
|
) as mock_reembed, patch(
|
|
"application.api.user.tasks.ingest.delay"
|
|
) as mock_ingest, patch(
|
|
"application.api.user.tasks.reingest_source_task.delay"
|
|
) as mock_reingest, app.test_request_context(
|
|
"/api/sources/wiki",
|
|
method="POST",
|
|
json={"name": "Seeded", "initial_content": "# Hello\nworld"},
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = CreateWikiSource().post()
|
|
|
|
assert response.status_code == 200
|
|
source_id = response.json["source_id"]
|
|
row = SourcesRepository(pg_conn).get_any(source_id, user)
|
|
assert int(row["tokens"]) > 0
|
|
# Re-embed fires once, only for the seed page; no ingest/reingest ever.
|
|
mock_reembed.assert_called_once()
|
|
assert mock_reembed.call_args.args[0] == source_id
|
|
assert mock_reembed.call_args.args[1] == WIKI_INDEX_PATH
|
|
assert mock_reembed.call_args.kwargs["user"] == user
|
|
mock_ingest.assert_not_called()
|
|
mock_reingest.assert_not_called()
|
|
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{source_id}/wiki/page?path={WIKI_INDEX_PATH}"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
page_response = WikiPage().get(source_id)
|
|
assert page_response.status_code == 200
|
|
assert page_response.json["page"]["content"] == "# Hello\nworld"
|
|
|
|
|
|
class TestWikiPages:
|
|
def test_returns_401_unauthenticated(self, app):
|
|
from application.api.user.sources.routes import WikiPages
|
|
|
|
with app.test_request_context("/api/sources/x/wiki/pages"):
|
|
from flask import request
|
|
request.decoded_token = None
|
|
response = WikiPages().get("x")
|
|
assert response.status_code == 401
|
|
|
|
def test_owner_lists_pages(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPages
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
|
|
|
|
user = "u-wiki-list"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki-list", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
WikiPagesRepository(pg_conn).upsert(
|
|
sid, "/index.md", "root", updated_by=user, updated_via="agent"
|
|
)
|
|
WikiPagesRepository(pg_conn).upsert(sid, "/docs/a.md", "a", updated_by=user)
|
|
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/pages"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPages().get(sid)
|
|
assert response.status_code == 200
|
|
paths = {p["path"] for p in response.json["pages"]}
|
|
assert paths == {"/index.md", "/docs/a.md"}
|
|
via = {p["path"]: p["updated_via"] for p in response.json["pages"]}
|
|
assert via["/index.md"] == "agent"
|
|
|
|
def test_non_owner_without_grant_404(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPages
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
owner = "u-wiki-owner"
|
|
stranger = "u-wiki-stranger"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki-private", user_id=owner, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/pages"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": stranger}
|
|
response = WikiPages().get(sid)
|
|
assert response.status_code == 404
|
|
|
|
def test_team_viewer_can_read(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPages
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
|
|
|
|
owner = "alice-wiki"
|
|
viewer = "bob-wiki-viewer"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki-shared", user_id=owner, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
WikiPagesRepository(pg_conn).upsert(sid, "/index.md", "x", updated_by=owner)
|
|
_grant_team_access(pg_conn, owner, viewer, sid, "viewer")
|
|
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/pages"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": viewer}
|
|
response = WikiPages().get(sid)
|
|
assert response.status_code == 200
|
|
assert {p["path"] for p in response.json["pages"]} == {"/index.md"}
|
|
|
|
|
|
class TestWikiPage:
|
|
def test_returns_400_missing_path(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-wiki-page-nopath"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPage().get(sid)
|
|
assert response.status_code == 400
|
|
|
|
def test_returns_400_traversal_path(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-wiki-page-traversal"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page?path=/../secret.md"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPage().get(sid)
|
|
assert response.status_code == 400
|
|
|
|
def test_returns_404_unknown_page(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-wiki-page-missing"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page?path=/nope.md"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPage().get(sid)
|
|
assert response.status_code == 404
|
|
|
|
def test_returns_provenance_and_version(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
|
|
|
|
user = "u-wiki-page-provenance"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
WikiPagesRepository(pg_conn).upsert(
|
|
sid, "/index.md", "x", updated_by=user, updated_via="agent"
|
|
)
|
|
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page?path=/index.md"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPage().get(sid)
|
|
assert response.status_code == 200
|
|
page = response.json["page"]
|
|
assert page["updated_via"] == "agent"
|
|
assert page["updated_by"] == user
|
|
assert page["version"] == 1
|
|
assert page["updated_at"] is not None
|
|
|
|
def test_non_owner_without_grant_404(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
|
|
|
|
owner = "u-wiki-pg-owner"
|
|
stranger = "u-wiki-pg-stranger"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=owner, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
WikiPagesRepository(pg_conn).upsert(sid, "/index.md", "secret", updated_by=owner)
|
|
|
|
with _patch_db(pg_conn), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page?path=/index.md"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": stranger}
|
|
response = WikiPage().get(sid)
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestConvertSourceToWiki:
|
|
def test_returns_401_unauthenticated(self, app):
|
|
from application.api.user.sources.routes import ConvertSourceToWiki
|
|
|
|
with app.test_request_context(
|
|
"/api/sources/x/wiki/convert", method="POST"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = None
|
|
response = ConvertSourceToWiki().post("x")
|
|
assert response.status_code == 401
|
|
|
|
def test_blank_source_enabled_inline_no_task(self, app, pg_conn):
|
|
from application.api.user.sources.routes import ConvertSourceToWiki
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.source_config import SourceConfig
|
|
|
|
user = "u-convert-blank"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"blank", user_id=user, type="file", directory_structure={}
|
|
)
|
|
sid = str(src["id"])
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.convert_source_to_wiki.delay"
|
|
) as mock_convert, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/convert", method="POST"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = ConvertSourceToWiki().post(sid)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json["converted"] is False
|
|
assert response.json["enabled"] is True
|
|
mock_convert.assert_not_called()
|
|
got = SourcesRepository(pg_conn).get_any(sid, user)
|
|
cfg = SourceConfig.parse(got.get("config"))
|
|
assert cfg.kind == "wiki"
|
|
assert cfg.retrieval.exposure == "agentic_tool"
|
|
|
|
def test_fileful_source_enqueues_task(self, app, pg_conn):
|
|
from application.api.user.sources.routes import ConvertSourceToWiki
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-convert-files"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"files", user_id=user, type="file",
|
|
directory_structure={"a.md": {"type": "text/markdown"}},
|
|
)
|
|
sid = str(src["id"])
|
|
|
|
fake_task = type("T", (), {"id": "task-xyz"})()
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.convert_source_to_wiki.delay",
|
|
return_value=fake_task,
|
|
) as mock_convert, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/convert", method="POST"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = ConvertSourceToWiki().post(sid)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json["task_id"] == "task-xyz"
|
|
mock_convert.assert_called_once()
|
|
assert mock_convert.call_args.kwargs["source_id"] == sid
|
|
assert mock_convert.call_args.kwargs["user"] == user
|
|
assert mock_convert.call_args.kwargs["idempotency_key"] == (
|
|
f"convert-wiki:{sid}"
|
|
)
|
|
|
|
def test_in_progress_ingest_rejected_409(self, app, pg_conn):
|
|
from application.api.user.sources.routes import ConvertSourceToWiki
|
|
from application.storage.db.repositories.ingest_chunk_progress import (
|
|
IngestChunkProgressRepository,
|
|
)
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-convert-ingesting"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"ingesting", user_id=user, type="file",
|
|
directory_structure={"a.md": {"type": "text/markdown"}},
|
|
)
|
|
sid = str(src["id"])
|
|
# An active embed in flight (embedded 0 of 5) → "processing".
|
|
IngestChunkProgressRepository(pg_conn).init_progress(sid, 5)
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.convert_source_to_wiki.delay"
|
|
) as mock_convert, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/convert", method="POST"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = ConvertSourceToWiki().post(sid)
|
|
|
|
assert response.status_code == 409
|
|
mock_convert.assert_not_called()
|
|
|
|
def test_viewer_rejected_403(self, app, pg_conn):
|
|
from application.api.user.sources.routes import ConvertSourceToWiki
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
owner = "alice-convert"
|
|
viewer = "bob-convert-viewer"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"files", user_id=owner, type="file",
|
|
directory_structure={"a.md": {"type": "text/markdown"}},
|
|
)
|
|
sid = str(src["id"])
|
|
_grant_team_access(pg_conn, owner, viewer, sid, "viewer")
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.convert_source_to_wiki.delay"
|
|
) as mock_convert, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/convert", method="POST"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": viewer}
|
|
response = ConvertSourceToWiki().post(sid)
|
|
|
|
assert response.status_code == 403
|
|
mock_convert.assert_not_called()
|
|
|
|
|
|
class TestWikiPageEdit:
|
|
def test_returns_401_unauthenticated(self, app):
|
|
from application.api.user.sources.routes import WikiPage
|
|
|
|
with app.test_request_context(
|
|
"/api/sources/x/wiki/page", method="PUT", json={}
|
|
):
|
|
from flask import request
|
|
request.decoded_token = None
|
|
response = WikiPage().put("x")
|
|
assert response.status_code == 401
|
|
|
|
def test_owner_writes_and_enqueues_reembed(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
|
|
|
|
user = "u-edit-owner"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.reembed_wiki_page.delay"
|
|
) as mock_reembed, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page",
|
|
method="PUT",
|
|
json={"path": "/notes.md", "content": "body"},
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPage().put(sid)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json["page"]["path"] == "/notes.md"
|
|
assert response.json["page"]["version"] is not None
|
|
assert response.json["page"]["updated_via"] == "human"
|
|
page = WikiPagesRepository(pg_conn).get_by_path(sid, "/notes.md")
|
|
assert page["content"] == "body"
|
|
assert page["updated_via"] == "human"
|
|
mock_reembed.assert_called_once()
|
|
assert mock_reembed.call_args.args[1] == "/notes.md"
|
|
assert mock_reembed.call_args.kwargs["user"] == user
|
|
|
|
def test_team_editor_reembeds_as_owner(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
owner = "alice-edit"
|
|
editor = "bob-edit-editor"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=owner, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
_grant_team_access(pg_conn, owner, editor, sid, "editor")
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.reembed_wiki_page.delay"
|
|
) as mock_reembed, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page",
|
|
method="PUT",
|
|
json={"path": "/e.md", "content": "edited"},
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": editor}
|
|
response = WikiPage().put(sid)
|
|
|
|
assert response.status_code == 200
|
|
# Re-embed runs AS the owner so the owner-scoped worker load resolves.
|
|
assert mock_reembed.call_args.kwargs["user"] == owner
|
|
|
|
def test_stale_version_returns_409(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
|
|
|
|
user = "u-edit-conflict"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
WikiPagesRepository(pg_conn).upsert(sid, "/c.md", "v1", updated_by=user)
|
|
# Bump to version 2 so a stale expected_version=1 loses the race.
|
|
WikiPagesRepository(pg_conn).upsert(sid, "/c.md", "v2", updated_by=user)
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.reembed_wiki_page.delay"
|
|
) as mock_reembed, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page",
|
|
method="PUT",
|
|
json={"path": "/c.md", "content": "v3", "expected_version": 1},
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPage().put(sid)
|
|
|
|
assert response.status_code == 409
|
|
mock_reembed.assert_not_called()
|
|
|
|
def test_traversal_path_returns_400(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
user = "u-edit-traversal"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=user, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.reembed_wiki_page.delay"
|
|
), app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page",
|
|
method="PUT",
|
|
json={"path": "/../secret.md", "content": "x"},
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = WikiPage().put(sid)
|
|
assert response.status_code == 400
|
|
|
|
def test_viewer_rejected_403(self, app, pg_conn):
|
|
from application.api.user.sources.routes import WikiPage
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
|
|
owner = "alice-edit-viewer"
|
|
viewer = "bob-edit-viewer"
|
|
src = SourcesRepository(pg_conn).create(
|
|
"wiki", user_id=owner, type="wiki", config={"kind": "wiki"}
|
|
)
|
|
sid = str(src["id"])
|
|
_grant_team_access(pg_conn, owner, viewer, sid, "viewer")
|
|
|
|
with _patch_db(pg_conn), patch(
|
|
"application.api.user.sources.routes.reembed_wiki_page.delay"
|
|
) as mock_reembed, app.test_request_context(
|
|
f"/api/sources/{sid}/wiki/page",
|
|
method="PUT",
|
|
json={"path": "/v.md", "content": "x"},
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": viewer}
|
|
response = WikiPage().put(sid)
|
|
|
|
assert response.status_code == 403
|
|
mock_reembed.assert_not_called()
|