mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 02:08:13 +00:00
documents_api: implement OpenCDE Documents API 1.0
POST /documents/1.0/document-versions resolves document_ids (base64url encoding of owner/repo/path) to versioned download URLs. Exposes only git tags + the current HEAD commit — mirrors CDE conventions where only released versions are visible, while still surfacing the WIP state. version_number is the tag name for releases and short SHA for HEAD. Tagged SHAs are deduplicated so HEAD at a tag doesn't create a duplicate. Download URLs point at Forgejo's existing raw/commit endpoint. Auth forwarded verbatim. No Go code, no Forgejo recompilation. Proxy at /documents/ on the Forgejo hostname. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8821c70098
commit
63a6b2bfa6
5 changed files with 343 additions and 1 deletions
|
|
@ -98,7 +98,7 @@
|
|||
{"id":"ifcurl-0oj","title":"Phase 4: Bonsai ifc:// OS protocol handler","description":"Register ifc:// as an OS protocol handler in Bonsai on installation (Windows, macOS, Linux). On receiving a URL, resolve the git source, load the model, and apply the view state from the URL parameters (camera, selector, clipping planes, visibility mode). This is the foundation for all other Phase 4 work.","status":"deferred","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:45Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:09Z","defer_until":"2026-12-01T00:00:00Z","dependency_count":0,"dependent_count":3,"comment_count":0}
|
||||
{"id":"ifcurl-cyw","title":"Phase 3c: Verify Forgejo merge-ability check works with ifcmerge","description":"Investigate whether Forgejo's merge-ability check (the 'branches can be merged automatically' indicator) correctly reflects ifcmerge's ability to handle .ifc files. Determine if Forgejo runs a trial git merge or uses a heuristic. If needed, patch Forgejo to run a trial git merge --no-commit to get an accurate result.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T08:27:25Z","started_at":"2026-04-24T08:19:58Z","closed_at":"2026-04-24T08:27:25Z","close_reason":"Verified: git merge-tree --write-tree (Forgejo's code path for git ≥2.38) does invoke merge drivers. Bare repos require core.attributesFile in /etc/gitconfig — committed .gitattributes not read. Added gitattributes server-config file and updated deployment docs.","dependencies":[{"issue_id":"ifcurl-cyw","depends_on_id":"ifcurl-c69","type":"blocks","created_at":"2026-04-23T06:48:29Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-c69","title":"Phase 3c: Register ifcmerge as git merge driver on Forgejo server","description":"Configure ifcmerge as the git merge driver for .ifc files on the Forgejo server. Add to /etc/gitconfig or ~forgejo/.gitconfig: [merge \"ifcmerge\"] name = IFC merge driver / driver = ifcmerge %O %A %B %L. Add *.ifc merge=ifcmerge to server-side gitattributes. This allows Forgejo's 'merge automatically' path to invoke ifcmerge via git when merging .ifc files.","status":"closed","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:21Z","created_by":"Bruno Postle","updated_at":"2026-04-24T08:15:20Z","closed_at":"2026-04-24T08:15:20Z","close_reason":"Documented ifcmerge driver registration (gitconfig-ifcmerge), gitattributes setup, merge direction constraint, and merge-commit strategy requirement","comments":[{"id":"019dbc54-bf0a-76e2-b64d-7d1681f0c730","issue_id":"ifcurl-c69","author":"Bruno Postle","text":"ifcmerge needs to be installed as a pair of merge tools because the merge process is asymmetrical and we need to be able to prefer one branch over another (because step-ids from one branch get rewritten in the merged file). In practice we prefer origin and/or main/master branches. there is code in the ifcopenshell bonsai ifcgit module for reference.","created_at":"2026-04-23T21:52:44Z"}],"dependency_count":0,"dependent_count":1,"comment_count":1}
|
||||
{"id":"ifcurl-gxf","title":"Add minimal read-only OpenCDE Documents API","description":"Expose a minimal read-only OpenCDE Documents API that makes IFC files in git repos addressable as OpenCDE documents.\n\n## Architecture\n\nGo routes added to Forgejo alongside the existing patch. Purely additive — new endpoints only. Uses Forgejo's existing auth middleware so per-user repository permissions are enforced correctly with no extra machinery. Direct access to Forgejo's repo and git internals — no round-trips through the REST API.\n\n## Design\n\ndocument_id encodes owner/repo/path/commit-hash as a stable opaque string. version_index is derived from the commit's position in the git log. Implements POST /document-versions returning download URLs pointing to Forgejo's existing raw file endpoint for the resolved commit.\n\nThis validates the ifc:// CDE extension in SPECIFICATION.md §9 (document_id= parameter) and makes Forgejo discoverable as an OpenCDE server to any client that speaks the Documents API.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-28T22:41:30Z","created_by":"Bruno Postle","updated_at":"2026-04-28T23:10:57Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-gxf","title":"Add minimal read-only OpenCDE Documents API","description":"Expose a minimal read-only OpenCDE Documents API that makes IFC files in git repos addressable as OpenCDE documents.\n\n## Architecture\n\nPython routes in the preview service, proxied at /documents/ on the Forgejo hostname via nginx/Caddy. Same pattern as the BCF API. Auth forwarded verbatim (Authorization: Bearer) — Forgejo enforces per-user repo permissions. No Go code, no Forgejo recompilation.\n\n## Design\n\ndocument_id is a base64url encoding of 'owner/repo/path', making it stable and opaque. version_index is the 1-based position of the commit in the file's git history (oldest=1, newest=N), derived by querying Forgejo's commits API with the file path filter.\n\nPOST /documents/1.0/document-versions returns all versions (up to 50 commits) for each requested document_id. Download URLs point at Forgejo's existing raw file endpoint: {forgejo_host}/{owner}/{repo}/raw/commit/{sha}/{path}.\n\nThe public Forgejo base URL for download links is derived from X-Forwarded-Host/X-Forwarded-Proto headers, same as the Foundation endpoint.\n\nThis validates the ifc:// CDE extension in SPECIFICATION.md §9 (document_id= parameter) and makes Forgejo discoverable as an OpenCDE server to any client that speaks the Documents API.","status":"in_progress","priority":4,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-28T22:41:30Z","created_by":"Bruno Postle","updated_at":"2026-04-29T18:48:16Z","started_at":"2026-04-29T18:48:04Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-s1o","title":"viewer: extract inline JS from viewer.html into viewer.js","description":"viewer.html is ~1250 lines, almost entirely JavaScript. This makes it hard to edit with proper tooling (no syntax highlighting, linting, or go-to-definition). Extract the main viewer logic into a separate viewer.js file alongside viewer-url.js. The build.js step would need to copy it to the assets directory.","status":"closed","priority":4,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-25T23:16:36Z","created_by":"Bruno Postle","updated_at":"2026-04-26T14:49:05Z","started_at":"2026-04-26T13:10:02Z","closed_at":"2026-04-26T14:49:05Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-nd1","title":"Submit provisional IANA registration for ifc:// scheme","description":"Once the Phase 1 URL spec is stable, submit a provisional IANA URI scheme registration for ifc://.\\n\\nProcess: email iana@iana.org with scheme name, syntax description, security considerations, and a link to the spec (GitHub URL suffices). First Come First Served — no committee review, typically processed within days to weeks. See RFC 7595 for requirements.\\n\\nThis secures the namespace, establishes priority, and is a prerequisite if the scheme is ever proposed to buildingSMART as an open standard.","status":"open","priority":4,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-25T11:17:45Z","created_by":"Bruno Postle","updated_at":"2026-04-25T11:17:45Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-u6r","title":"Forgejo commit page: no IFC thumbnail — git log gives no visual indication of model state","description":"In the Forgejo repository commit list, each commit shows only the commit message. When a commit modifies an IFC file, there is no thumbnail showing what the model looks like. Reviewers browsing commits have no visual cues about which commits changed significant geometry versus metadata-only changes. A Forgejo webhook or CI integration could post a rendered PNG thumbnail as a commit status/check comment when .ifc files change, using the preview service. This would make the git log visually meaningful for BIM projects.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:41Z","created_by":"Bruno Postle","updated_at":"2026-04-25T20:07:21Z","closed_at":"2026-04-25T20:07:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ ifcurl serve --host 0.0.0.0 --port 9000 --allowed-hosts git.example.com
|
|||
| `GET /select?url=ifc://…` | Resolve a complex selector server-side; returns JSON list of GlobalIds |
|
||||
| `GET /render_diff?base=ifc://…&head=ifc://…` | Render a colour-coded diff PNG (added green, removed red) |
|
||||
| `GET /foundation/versions` | OpenCDE Foundation API discovery — lists BCF and Documents API endpoints (proxy at `/foundation/` on the Forgejo hostname) |
|
||||
| `POST /documents/1.0/document-versions` | OpenCDE Documents API — resolve document_ids to versioned download URLs (proxied at `/documents/`) |
|
||||
| `GET /bcf/3.0/projects` | BCF 3.0 — list repositories as BCF projects |
|
||||
| `GET/POST /bcf/3.0/projects/{owner}/{repo}/topics` | BCF 3.0 — list or create topics (Forgejo issues) |
|
||||
| `GET/PUT /bcf/3.0/projects/{owner}/{repo}/topics/{guid}` | BCF 3.0 — get or update a topic |
|
||||
|
|
@ -159,6 +160,7 @@ The BCF API forwards the client's `Authorization: Bearer` header to Forgejo's RE
|
|||
```nginx
|
||||
location /foundation/ { proxy_pass http://localhost:8000/foundation/; }
|
||||
location /bcf/ { proxy_pass http://localhost:8000/bcf/; }
|
||||
location /documents/ { proxy_pass http://localhost:8000/documents/; }
|
||||
```
|
||||
|
||||
**Caching:**
|
||||
|
|
|
|||
160
ifcurl/documents_api.py
Normal file
160
ifcurl/documents_api.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# IFC URL — OpenCDE Documents API routes
|
||||
# Copyright (C) 2026 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IFC URL.
|
||||
#
|
||||
# IFC URL is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IFC URL is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IFC URL. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""OpenCDE Documents API 1.0 — read-only, minimal implementation.
|
||||
|
||||
Routes are mounted at /documents/1.0 in service.py and proxied at
|
||||
/documents/ on the Forgejo hostname via nginx/Caddy.
|
||||
|
||||
document_id is a base64url encoding of "owner/repo/path", making it stable
|
||||
and opaque to callers.
|
||||
|
||||
Versions exposed: one per git tag (sorted chronologically, oldest = index 1),
|
||||
plus the most recent HEAD commit if it is not already covered by a tag. This
|
||||
mirrors CDE conventions where only released/tagged versions are visible, while
|
||||
still surfacing the current work-in-progress state.
|
||||
|
||||
version_number is the tag name for tagged versions and the short SHA for HEAD.
|
||||
|
||||
Download URLs point at Forgejo's existing raw file endpoint:
|
||||
{forgejo_host}/{owner}/{repo}/raw/commit/{sha}/{path}
|
||||
|
||||
Auth is forwarded verbatim so Forgejo enforces per-user permissions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
|
||||
from fastapi import APIRouter, Body, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from ifcurl.bcf_api import _auth, _fget
|
||||
|
||||
router = APIRouter(prefix="/documents/1.0")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# document_id encoding / decoding
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def encode_document_id(owner: str, repo: str, path: str) -> str:
|
||||
"""Encode owner/repo/path as a stable base64url document_id."""
|
||||
return base64.urlsafe_b64encode(f"{owner}/{repo}/{path}".encode()).rstrip(b"=").decode()
|
||||
|
||||
|
||||
def decode_document_id(doc_id: str) -> tuple[str, str, str]:
|
||||
"""Decode a document_id back to (owner, repo, path). Raises ValueError if malformed."""
|
||||
padding = "=" * (4 - len(doc_id) % 4) if len(doc_id) % 4 else ""
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(doc_id + padding).decode()
|
||||
except Exception as exc:
|
||||
raise ValueError(f"Invalid document_id: {doc_id!r}") from exc
|
||||
parts = decoded.split("/", 2)
|
||||
if len(parts) != 3 or not all(parts):
|
||||
raise ValueError(f"Invalid document_id: {doc_id!r}")
|
||||
return parts[0], parts[1], parts[2]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Version enumeration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _versions_for_file(owner: str, repo: str, path: str, auth: str | None, forgejo_base: str) -> list[dict]:
|
||||
"""Return a list of BCF version dicts for the file, tagged releases + HEAD."""
|
||||
# Tagged versions — one entry per tag, sorted oldest-first by commit date.
|
||||
tags = _fget(f"/api/v1/repos/{owner}/{repo}/tags", auth, params={"limit": 50})
|
||||
tags = tags if isinstance(tags, list) else []
|
||||
|
||||
seen_shas: set[str] = set()
|
||||
versions: list[dict] = []
|
||||
|
||||
for tag in sorted(tags, key=lambda t: t.get("commit", {}).get("created", "")):
|
||||
sha = (tag.get("commit") or {}).get("sha", "")
|
||||
if not sha or sha in seen_shas:
|
||||
continue
|
||||
seen_shas.add(sha)
|
||||
versions.append({
|
||||
"sha": sha,
|
||||
"date": (tag.get("commit") or {}).get("created", ""),
|
||||
"version_number": tag.get("name", sha[:8]),
|
||||
"author_name": "",
|
||||
"author_email": "",
|
||||
})
|
||||
|
||||
# HEAD — add only if its commit SHA is not already represented by a tag.
|
||||
head_commits = _fget(
|
||||
f"/api/v1/repos/{owner}/{repo}/commits",
|
||||
auth,
|
||||
params={"path": path, "limit": 1, "sha": "HEAD"},
|
||||
)
|
||||
if isinstance(head_commits, list) and head_commits:
|
||||
head = head_commits[0]
|
||||
sha = head.get("sha", "")
|
||||
if sha and sha not in seen_shas:
|
||||
commit_meta = head.get("commit") or {}
|
||||
author = commit_meta.get("author") or {}
|
||||
committer = commit_meta.get("committer") or {}
|
||||
versions.append({
|
||||
"sha": sha,
|
||||
"date": committer.get("date") or author.get("date", ""),
|
||||
"version_number": sha[:8],
|
||||
"author_name": author.get("name", ""),
|
||||
"author_email": author.get("email", ""),
|
||||
})
|
||||
|
||||
return [
|
||||
{
|
||||
"version_index": i,
|
||||
"version_number": v["version_number"],
|
||||
"creation_date": v["date"],
|
||||
"created_by": {"id": v["author_email"], "name": v["author_name"]},
|
||||
"document_version_download": f"{forgejo_base}/{owner}/{repo}/raw/commit/{v['sha']}/{path}",
|
||||
"document_version_upload": None,
|
||||
}
|
||||
for i, v in enumerate(versions, start=1)
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Route
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.post("/document-versions")
|
||||
def document_versions(request: Request, body: dict = Body(default={})) -> JSONResponse:
|
||||
"""Return tagged releases + HEAD for each requested document_id.
|
||||
|
||||
Implements the OpenCDE Documents API resolution step (SPECIFICATION.md §9
|
||||
step 3). Tagged versions are indexed oldest=1, newest=N; HEAD is appended
|
||||
as N+1 when it is not already tagged. version_number is the tag name for
|
||||
releases and the short SHA for HEAD.
|
||||
"""
|
||||
auth = _auth(request)
|
||||
host = request.headers.get("x-forwarded-host") or request.headers.get("host", "localhost")
|
||||
proto = request.headers.get("x-forwarded-proto", "https")
|
||||
forgejo_base = f"{proto}://{host}"
|
||||
|
||||
result = []
|
||||
for doc_id in body.get("document_ids", []):
|
||||
try:
|
||||
owner, repo, path = decode_document_id(doc_id)
|
||||
except ValueError:
|
||||
continue
|
||||
for v in _versions_for_file(owner, repo, path, auth, forgejo_base):
|
||||
result.append({"document_id": doc_id, **v})
|
||||
|
||||
return JSONResponse(result)
|
||||
|
|
@ -53,6 +53,7 @@ from pydantic import BaseModel
|
|||
from ifcurl.auth import get_token_for_host
|
||||
from ifcurl.bcf import build_bcf
|
||||
from ifcurl.bcf_api import router as bcf_router
|
||||
from ifcurl.documents_api import router as documents_router
|
||||
from ifcurl.git import diff_text as git_diff_text
|
||||
from ifcurl.git import fetch_ifc
|
||||
from ifcurl.render_service import _sandboxed_diff, _sandboxed_pipeline, _sandboxed_select
|
||||
|
|
@ -174,6 +175,7 @@ app = FastAPI(
|
|||
version="0.0.0",
|
||||
)
|
||||
app.include_router(bcf_router)
|
||||
app.include_router(documents_router)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rate limiting (in-process, per client IP, sliding window)
|
||||
|
|
|
|||
178
tests/test_documents_api.py
Normal file
178
tests/test_documents_api.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""Tests for ifcurl.documents_api — OpenCDE Documents API routes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ifcurl.documents_api import decode_document_id, encode_document_id
|
||||
from ifcurl.service import _rate_hits, app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
OWNER, REPO, PATH = "alice", "myproject", "models/building.ifc"
|
||||
|
||||
TAG_V1 = {
|
||||
"name": "v1.0",
|
||||
"commit": {"sha": "1111aaaa2222bbbb3333cccc4444dddd5555eeee", "created": "2026-01-01T10:00:00Z"},
|
||||
}
|
||||
TAG_V2 = {
|
||||
"name": "v2.0",
|
||||
"commit": {"sha": "aaaa1111bbbb2222cccc3333dddd4444eeee5555", "created": "2026-02-01T10:00:00Z"},
|
||||
}
|
||||
HEAD_COMMIT = {
|
||||
"sha": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
"commit": {
|
||||
"author": {"name": "Alice", "email": "alice@example.com", "date": "2026-03-01T10:00:00Z"},
|
||||
"committer": {"name": "Alice", "email": "alice@example.com", "date": "2026-03-01T10:00:00Z"},
|
||||
"message": "WIP changes",
|
||||
},
|
||||
}
|
||||
# HEAD at same SHA as the latest tag — should not add a duplicate version
|
||||
HEAD_AT_TAG = {
|
||||
"sha": TAG_V2["commit"]["sha"],
|
||||
"commit": {
|
||||
"author": {"name": "Alice", "email": "alice@example.com", "date": "2026-02-01T10:00:00Z"},
|
||||
"committer": {"name": "Alice", "email": "alice@example.com", "date": "2026-02-01T10:00:00Z"},
|
||||
"message": "Release v2.0",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_rate_limit():
|
||||
_rate_hits.clear()
|
||||
yield
|
||||
_rate_hits.clear()
|
||||
|
||||
|
||||
def _post(doc_ids, tags=None, head_commits=None, headers=None):
|
||||
if tags is None:
|
||||
tags = [TAG_V1, TAG_V2]
|
||||
if head_commits is None:
|
||||
head_commits = [HEAD_COMMIT]
|
||||
|
||||
def mock_fget(path, auth, params=None):
|
||||
if "/tags" in path:
|
||||
return tags
|
||||
if "/commits" in path:
|
||||
return head_commits
|
||||
return []
|
||||
|
||||
with patch("ifcurl.documents_api._fget", side_effect=mock_fget):
|
||||
return client.post(
|
||||
"/documents/1.0/document-versions",
|
||||
json={"document_ids": doc_ids},
|
||||
headers=headers or {},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# document_id encoding
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDocumentId:
|
||||
def test_roundtrip(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
assert decode_document_id(doc_id) == (OWNER, REPO, PATH)
|
||||
|
||||
def test_path_with_slashes(self):
|
||||
doc_id = encode_document_id("alice", "proj", "a/b/c.ifc")
|
||||
assert decode_document_id(doc_id) == ("alice", "proj", "a/b/c.ifc")
|
||||
|
||||
def test_stable(self):
|
||||
assert encode_document_id(OWNER, REPO, PATH) == encode_document_id(OWNER, REPO, PATH)
|
||||
|
||||
def test_different_inputs_differ(self):
|
||||
assert encode_document_id("alice", "a", "m.ifc") != encode_document_id("alice", "b", "m.ifc")
|
||||
|
||||
def test_invalid_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
decode_document_id("!!!not-base64!!!")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /documents/1.0/document-versions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDocumentVersions:
|
||||
def test_returns_200(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_two_tags_plus_head_give_three_versions(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
assert len(r.json()) == 3
|
||||
|
||||
def test_tag_names_used_as_version_number(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
version_numbers = {v["version_number"] for v in r.json()}
|
||||
assert "v1.0" in version_numbers
|
||||
assert "v2.0" in version_numbers
|
||||
|
||||
def test_head_version_number_is_short_sha(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
versions = sorted(r.json(), key=lambda v: v["version_index"])
|
||||
assert versions[-1]["version_number"] == HEAD_COMMIT["sha"][:8]
|
||||
|
||||
def test_oldest_tag_is_version_index_1(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
versions = sorted(r.json(), key=lambda v: v["version_index"])
|
||||
assert versions[0]["version_number"] == "v1.0"
|
||||
|
||||
def test_head_at_same_sha_as_tag_not_duplicated(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id], head_commits=[HEAD_AT_TAG])
|
||||
assert len(r.json()) == 2 # only the two tags, HEAD deduplicated
|
||||
|
||||
def test_no_tags_just_head(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id], tags=[])
|
||||
versions = r.json()
|
||||
assert len(versions) == 1
|
||||
assert versions[0]["version_number"] == HEAD_COMMIT["sha"][:8]
|
||||
|
||||
def test_download_url_uses_commit_sha(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
for v in r.json():
|
||||
assert "/raw/commit/" in v["document_version_download"]
|
||||
|
||||
def test_download_url_contains_path(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
for v in r.json():
|
||||
assert PATH in v["document_version_download"]
|
||||
|
||||
def test_download_url_uses_forwarded_host(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id], headers={"x-forwarded-host": "git.example.com", "x-forwarded-proto": "https"})
|
||||
for v in r.json():
|
||||
assert v["document_version_download"].startswith("https://git.example.com/")
|
||||
|
||||
def test_document_id_preserved(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id])
|
||||
for v in r.json():
|
||||
assert v["document_id"] == doc_id
|
||||
|
||||
def test_invalid_document_id_skipped(self):
|
||||
valid_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post(["not-valid!!!!", valid_id])
|
||||
assert all(v["document_id"] == valid_id for v in r.json())
|
||||
|
||||
def test_empty_document_ids(self):
|
||||
r = _post([])
|
||||
assert r.json() == []
|
||||
|
||||
def test_no_commits_and_no_tags_gives_no_versions(self):
|
||||
doc_id = encode_document_id(OWNER, REPO, PATH)
|
||||
r = _post([doc_id], tags=[], head_commits=[])
|
||||
assert r.json() == []
|
||||
Loading…
Add table
Reference in a new issue