diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 542c265..23a8ef0 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,12 +1,12 @@ -{"id":"ifcurl-4l7","title":"viewer.html: scale= URLs don't load in orthographic mode","description":"applyCameraParam() reads the camera= and fov= parameters from the URL and applies them, but never reads scale= or switches the camera to orthographic (parallel) projection. URLs like ?camera=...\u0026scale=50 will load in perspective mode instead of orthographic. The OBC OrthoPerspectiveCamera can switch modes, but the code path to set parallel projection and apply the scale value is missing from applyCameraParam.","status":"open","priority":1,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:51Z","created_by":"Bruno Postle","updated_at":"2026-04-24T09:04:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-362","title":"Add missing server-config files: gitconfig-ifcmerge and gitattributes","description":"forgejo/README.md documents deploying two files that don't exist in the repo: forgejo/server-config/gitconfig-ifcmerge and forgejo/server-config/gitattributes. Both are required for the ifcmerge git merge driver to work in bare Forgejo repos. The README even includes 'sudo cp forgejo/server-config/gitattributes /etc/gitattributes' and 'sudo git config --system include.path .../gitconfig-ifcmerge', but the files themselves are absent. Anyone following the deployment instructions will hit a 'file not found' error. Need to create both files with the content described in the README.","status":"open","priority":1,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:31Z","created_by":"Bruno Postle","updated_at":"2026-04-24T09:04:31Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-9rk","title":"pyproject.toml: bcf package not listed as optional service dependency","description":"service.py and bcf.py work correctly because the 'bcf' Python library is installed, but it is not listed in pyproject.toml's [service] or [render] optional dependencies. A fresh install with 'pip install ifcurl[service]' may not install it. Add bcf to the [service] extras to make the dependency explicit.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:19Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:41:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-4l7","title":"viewer.html: scale= URLs don't load in orthographic mode","description":"applyCameraParam() reads the camera= and fov= parameters from the URL and applies them, but never reads scale= or switches the camera to orthographic (parallel) projection. URLs like ?camera=...\u0026scale=50 will load in perspective mode instead of orthographic. The OBC OrthoPerspectiveCamera can switch modes, but the code path to set parallel projection and apply the scale value is missing from applyCameraParam.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:51Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:29Z","started_at":"2026-04-24T10:50:19Z","closed_at":"2026-04-24T10:56:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-362","title":"Add missing server-config files: gitconfig-ifcmerge and gitattributes","description":"forgejo/README.md documents deploying two files that don't exist in the repo: forgejo/server-config/gitconfig-ifcmerge and forgejo/server-config/gitattributes. Both are required for the ifcmerge git merge driver to work in bare Forgejo repos. The README even includes 'sudo cp forgejo/server-config/gitattributes /etc/gitattributes' and 'sudo git config --system include.path .../gitconfig-ifcmerge', but the files themselves are absent. Anyone following the deployment instructions will hit a 'file not found' error. Need to create both files with the content described in the README.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:31Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:51:47Z","started_at":"2026-04-24T10:50:15Z","closed_at":"2026-04-24T10:51:47Z","close_reason":"Files already exist: forgejo/server-config/gitconfig-ifcmerge and gitattributes are both present and correct. Missed in initial review.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-9rk","title":"pyproject.toml: bcf package not listed as optional service dependency","description":"service.py and bcf.py work correctly because the 'bcf' Python library is installed, but it is not listed in pyproject.toml's [service] or [render] optional dependencies. A fresh install with 'pip install ifcurl[service]' may not install it. Add bcf to the [service] extras to make the dependency explicit.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:19Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:27Z","started_at":"2026-04-24T10:50:11Z","closed_at":"2026-04-24T10:56:27Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-czd","title":"GET /preview has no token parameter — private repos inaccessible","description":"POST /preview accepts a token field in the JSON body for private repository authentication. GET /preview (used by Forgejo \u003cimg src=\u003e tags) only takes a url query parameter with no way to pass a token. The Go markdown extension generates img tags using the GET form. This means inline previews in Forgejo markdown can never authenticate to private repos — they will silently fail or return 404. Either the Go extension should use POST, or a separate signed/session-scoped token mechanism is needed for the GET path.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:17Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:40:17Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-3n4","title":"viewer-url.js: toRawUrl GitLab host detection too broad","description":"toRawUrl() uses host.includes('gitlab') to detect GitLab instances, which matches any hostname containing the string 'gitlab' — e.g. 'not-actually-gitlab.example.com' would be routed to the GitLab raw URL pattern. Should use a more specific check: host === 'gitlab.com' or host.startsWith('gitlab.') to avoid false positives on unrelated hostnames.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:01Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:40:01Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-7wg","title":"service.py: /bcf endpoint doesn't include source URL in BCF description","description":"The viewer's client-side BCF generation records the source ifc:// URL as a \u003cDescription\u003e element in the markup, making the BCF traceable back to its origin. The service POST /bcf endpoint calls build_bcf() without passing any description, so service-generated BCF files contain no reference to the original URL. The URL should be included (in Description or a comment) so the BCF is self-documenting. build_bcf() needs a description parameter, or the url should be passed as comment/description automatically.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:17Z","created_by":"Bruno Postle","updated_at":"2026-04-24T09:05:17Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-sto","title":"viewer.html: BCF export ignores visibility mode from URL","description":"The viewer's client-side generateBcf() always writes '\u003cVisibility DefaultVisibility=\"true\"/\u003e' and never adds Selection or Exceptions elements based on the visibility= URL parameter. A URL with visibility=isolate or visibility=ghost generates BCF with no component visibility data, so a BCF tool importing the file will show all elements with no selection. The service /bcf endpoint handles this correctly. The viewer-side BCF should at minimum write the selector string as a Description so the visibility intent is preserved.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:14Z","created_by":"Bruno Postle","updated_at":"2026-04-24T09:05:14Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-t1k","title":"git.py: _evict_if_needed not called after fetch, only after clone","description":"_evict_if_needed() is called in _open_remote() only after a fresh clone (the 'if not git_dir.exists()' branch). When a mutable ref triggers a git fetch on an existing clone, the repo grows with new objects but eviction is never checked. Over time a long-running service can exceed IFCURL_CACHE_MAX_GB without triggering cleanup. Fix: call _evict_if_needed() after fetch as well as after clone.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:03Z","created_by":"Bruno Postle","updated_at":"2026-04-24T09:05:03Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-ttb","title":"README.md: documents GET /viewer endpoint that doesn't exist","description":"The preview service endpoints table in README.md lists 'GET /viewer?url=ifc://… — Redirect to the Forgejo browser viewer' but no such route exists in ifcurl/service.py. The viewer is served directly by Forgejo at /assets/viewer.html. The service only has POST /preview, GET /preview, and POST /bcf. Remove the bogus row or implement the redirect.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:00Z","created_by":"Bruno Postle","updated_at":"2026-04-24T09:05:00Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-3n4","title":"viewer-url.js: toRawUrl GitLab host detection too broad","description":"toRawUrl() uses host.includes('gitlab') to detect GitLab instances, which matches any hostname containing the string 'gitlab' — e.g. 'not-actually-gitlab.example.com' would be routed to the GitLab raw URL pattern. Should use a more specific check: host === 'gitlab.com' or host.startsWith('gitlab.') to avoid false positives on unrelated hostnames.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:01Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:28Z","started_at":"2026-04-24T10:50:14Z","closed_at":"2026-04-24T10:56:28Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-7wg","title":"service.py: /bcf endpoint doesn't include source URL in BCF description","description":"The viewer's client-side BCF generation records the source ifc:// URL as a \u003cDescription\u003e element in the markup, making the BCF traceable back to its origin. The service POST /bcf endpoint calls build_bcf() without passing any description, so service-generated BCF files contain no reference to the original URL. The URL should be included (in Description or a comment) so the BCF is self-documenting. build_bcf() needs a description parameter, or the url should be passed as comment/description automatically.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:17Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:29Z","started_at":"2026-04-24T10:50:17Z","closed_at":"2026-04-24T10:56:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-sto","title":"viewer.html: BCF export ignores visibility mode from URL","description":"The viewer's client-side generateBcf() always writes '\u003cVisibility DefaultVisibility=\"true\"/\u003e' and never adds Selection or Exceptions elements based on the visibility= URL parameter. A URL with visibility=isolate or visibility=ghost generates BCF with no component visibility data, so a BCF tool importing the file will show all elements with no selection. The service /bcf endpoint handles this correctly. The viewer-side BCF should at minimum write the selector string as a Description so the visibility intent is preserved.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:14Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:29Z","started_at":"2026-04-24T10:50:18Z","closed_at":"2026-04-24T10:56:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-t1k","title":"git.py: _evict_if_needed not called after fetch, only after clone","description":"_evict_if_needed() is called in _open_remote() only after a fresh clone (the 'if not git_dir.exists()' branch). When a mutable ref triggers a git fetch on an existing clone, the repo grows with new objects but eviction is never checked. Over time a long-running service can exceed IFCURL_CACHE_MAX_GB without triggering cleanup. Fix: call _evict_if_needed() after fetch as well as after clone.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:03Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:28Z","started_at":"2026-04-24T10:50:16Z","closed_at":"2026-04-24T10:56:28Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-ttb","title":"README.md: documents GET /viewer endpoint that doesn't exist","description":"The preview service endpoints table in README.md lists 'GET /viewer?url=ifc://… — Redirect to the Forgejo browser viewer' but no such route exists in ifcurl/service.py. The viewer is served directly by Forgejo at /assets/viewer.html. The service only has POST /preview, GET /preview, and POST /bcf. Remove the bogus row or implement the redirect.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:00Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:27Z","started_at":"2026-04-24T10:50:13Z","closed_at":"2026-04-24T10:56:27Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-2v9","title":"Security audit: preview endpoint SSRF risk","description":"The /preview endpoint accepts an ifc:// URL and calls fetch_ifc(), which clones or fetches from whatever git host is named in the URL. This is a potential SSRF vector: a caller could supply ifc://internal-host/... to trigger outbound connections to internal infrastructure. Assess: (1) should the service validate that the host is in an allowlist or is a public forge? (2) are there other parameters that could be abused? (3) is the intended deployment model (co-located with Forgejo, not exposed publicly) a sufficient mitigation, or should we add explicit controls regardless? Document the threat model and add any necessary validation.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T07:09:33Z","created_by":"Bruno Postle","updated_at":"2026-04-24T07:21:07Z","started_at":"2026-04-24T07:12:29Z","closed_at":"2026-04-24T07:21:07Z","close_reason":"Implemented: reject local transport (403), --allowed-hosts allowlist for serve command, and private-IP literal blocking as defense-in-depth","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-bab","title":"Implement clip= parameter in viewer.html","description":"The ifc:// spec defines a repeatable clip= parameter (6 floats: point x,y,z + normal x,y,z in IFC world coords). The Python ifcurl renderer handles it, but viewer.html ignores it entirely — no parsing, no Three.js clipping planes, no round-trip into the URL on camera sync. Implement: parse clip= from the ifc:// URL, apply as THREE.Plane clipping planes to the renderer (with IFC→Three.js coordinate transform), and include current clip planes when rebuilding the URL.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:58Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:09:31Z","started_at":"2026-04-24T06:08:44Z","closed_at":"2026-04-24T06:09:31Z","close_reason":"applyClipPlanes() parses all clip= params, transforms IFC→Three.js coords via toThree(), constructs THREE.Plane objects and sets on world.renderer.three.clippingPlanes. syncCameraUrl already preserves clip= params unchanged.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-yju","title":"viewer.html: structured form UI for ifc:// URL components","description":"Replace the single ifc:// URL text input with individual labelled fields for each component: host/repository, ref (branch/tag/commit), path (file within repo), selector (IfcOpenShell selector), camera, fov/scale. Editing any field should update the ifc:// URL and reload the model. The raw ifc:// URL should still be visible/copyable. This makes the viewer usable without needing to hand-edit a URL string.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T21:07:11Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:32:17Z","started_at":"2026-04-23T21:12:42Z","closed_at":"2026-04-23T21:32:17Z","close_reason":"Structured form UI implemented with repo/ref/path/selector fields; crash recovered, changes committed.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/README.md b/README.md index 4b1805c..4c5bcb3 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,8 @@ non-private remote hosts. | Endpoint | Description | |---|---| | `POST /preview` | Render an ifc:// URL to PNG | -| `GET /preview?url=ifc://…` | Same, via query string | -| `GET /viewer?url=ifc://…` | Redirect to the Forgejo browser viewer | +| `GET /preview?url=ifc://…` | Same, via query string (used by Forgejo `` tags) | +| `POST /bcf` | Generate a BCF 2.1 zip from an ifc:// URL viewpoint | ### Caching diff --git a/forgejo/custom/public/assets/viewer-url.js b/forgejo/custom/public/assets/viewer-url.js index 2b7d11e..798f387 100644 --- a/forgejo/custom/public/assets/viewer-url.js +++ b/forgejo/custom/public/assets/viewer-url.js @@ -69,7 +69,7 @@ export function toRawUrl(raw) { if (host === "github.com" || host.endsWith(".github.com")) { return `https://raw.githubusercontent.com/${repoPath}/${plainRef}/${filePath}`; } - if (host === "gitlab.com" || host.includes("gitlab")) { + if (host === "gitlab.com" || host.startsWith("gitlab.")) { const proto = host.startsWith("localhost") ? "http" : "https"; return `${proto}://${host}/${repoPath}/-/raw/${plainRef}/${filePath}`; } diff --git a/forgejo/custom/public/assets/viewer.html b/forgejo/custom/public/assets/viewer.html index 7614223..3f34788 100644 --- a/forgejo/custom/public/assets/viewer.html +++ b/forgejo/custom/public/assets/viewer.html @@ -436,8 +436,19 @@ const target = pos.clone().add(dir); await controls.setLookAt(pos.x, pos.y, pos.z, target.x, target.y, target.z, false); - const fovStr = qs.get("fov"); - if (fovStr && camera.isPerspectiveCamera) { + const fovStr = qs.get("fov"); + const scaleStr = qs.get("scale"); + if (scaleStr) { + // Switch to orthographic projection and apply the view-to-world scale. + await controls.setOrthoCamera(); + const s = parseFloat(scaleStr); + if (!isNaN(s)) { + camera.top = s / 2; + camera.bottom = -s / 2; + camera.updateProjectionMatrix(); + } + fovInput.value = ""; + } else if (fovStr && camera.isPerspectiveCamera) { camera.fov = Math.max(10, Math.min(120, parseFloat(fovStr))); camera.updateProjectionMatrix(); fovInput.value = Math.round(camera.fov); @@ -456,14 +467,15 @@ const qIdx = src.indexOf("?"); const qs = new URLSearchParams(qIdx < 0 ? "" : src.slice(qIdx + 1)); - const camV = (qs.get("camera") ?? "").split(",").map(Number); - const cam = camV.length === 9 && !camV.some(isNaN) ? camV : null; - const fov = cam ? (parseFloat(qs.get("fov")) || null) : null; - const scale = cam ? (parseFloat(qs.get("scale")) || null) : null; - const clips = qs.getAll("clip") + const camV = (qs.get("camera") ?? "").split(",").map(Number); + const cam = camV.length === 9 && !camV.some(isNaN) ? camV : null; + const fov = cam ? (parseFloat(qs.get("fov")) || null) : null; + const scale = cam ? (parseFloat(qs.get("scale")) || null) : null; + const clips = qs.getAll("clip") .map(s => s.split(",").map(Number)) .filter(v => v.length === 6 && !v.some(isNaN)); - const selector = qs.get("selector") || ""; + const selector = qs.get("selector") || ""; + const visibility = qs.get("visibility") || "highlight"; const topicGuid = crypto.randomUUID(); const vpGuid = cam ? crypto.randomUUID() : null; @@ -506,9 +518,12 @@ `${xyz("Location",cx,cy,cz)}${xyz("Direction",nx,ny,nz)}` ).join("\n ")} ` : ""; + // Reflect visibility mode: isolate → hide everything by default, + // ghost/highlight → show everything by default. + const defaultVis = visibility === "isolate" ? "false" : "true"; viewpointXml = ` - + ${camXml} ${clipsXml} `; diff --git a/ifcurl/bcf.py b/ifcurl/bcf.py index be45773..0953789 100644 --- a/ifcurl/bcf.py +++ b/ifcurl/bcf.py @@ -106,11 +106,14 @@ def _markup_xml( comment: str, author: str, now: str, + description: str = "", ) -> bytes: root = ET.Element("Markup") topic = ET.SubElement(root, "Topic", Guid=topic_guid, TopicType="Coordination", TopicStatus="Open") ET.SubElement(topic, "Title").text = title or "IFC View" + if description: + ET.SubElement(topic, "Description").text = description ET.SubElement(topic, "CreationDate").text = now ET.SubElement(topic, "CreationAuthor").text = author if vp_guid: @@ -137,6 +140,7 @@ def build_bcf( visibility: str = "highlight", title: str = "IFC View", comment: str = "", + description: str = "", author: str = "anonymous", ) -> bytes: """Build a BCF 2.1 zip archive and return the raw bytes. @@ -149,6 +153,7 @@ def build_bcf( :param visibility: ``'highlight'``, ``'ghost'``, or ``'isolate'``. :param title: BCF topic title. :param comment: Optional comment text added to the topic. + :param description: Optional long description for the topic (e.g. the source ifc:// URL). :param author: Author string recorded in the BCF markup. :returns: Bytes of a valid BCF 2.1 zip archive. """ @@ -161,7 +166,7 @@ def build_bcf( zf.writestr("bcf.version", _VERSION_XML) zf.writestr( f"{topic_guid}/markup.bcf", - _markup_xml(topic_guid, vp_guid, title, comment, author, now), + _markup_xml(topic_guid, vp_guid, title, comment, author, now, description), ) if vp_guid is not None: zf.writestr( diff --git a/ifcurl/git.py b/ifcurl/git.py index 3c0bf85..bd4a1d0 100644 --- a/ifcurl/git.py +++ b/ifcurl/git.py @@ -266,6 +266,7 @@ def _open_remote(remote_url: str, is_mutable: bool, token: str | None = None) -> repo.git.fetch("origin") except git.exc.GitCommandError: pass # offline — use cached data + _evict_if_needed() _touch_cache(cache_dir) return repo diff --git a/ifcurl/service.py b/ifcurl/service.py index b3d11cf..6877165 100644 --- a/ifcurl/service.py +++ b/ifcurl/service.py @@ -446,6 +446,7 @@ def bcf_export(request: BcfRequest) -> Response: visibility=ifc_url.visibility, title=request.title, comment=request.comment, + description=request.url, ) return Response( content=bcf_bytes, diff --git a/pyproject.toml b/pyproject.toml index b79f85d..d5baaf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = ["ifcopenshell", "gitpython", "platformdirs"] [project.optional-dependencies] render = ["pyvista", "numpy"] -service = ["pyvista", "numpy", "fastapi", "uvicorn[standard]"] +service = ["pyvista", "numpy", "fastapi", "uvicorn[standard]", "bcf"] [project.scripts] ifcurl = "ifcurl.__main__:main" diff --git a/tests/test_bcf.py b/tests/test_bcf.py index 46f0ea3..ef60437 100644 --- a/tests/test_bcf.py +++ b/tests/test_bcf.py @@ -128,6 +128,18 @@ class TestBuildBcf: assert 'DefaultVisibility="false"' in content assert "Exceptions" in content + def test_description_in_markup(self): + data = build_bcf(description="ifc://example.com/org/repo@heads/main?path=m.ifc") + markup = next(n for n in _zip_names(data) if n.endswith("markup.bcf")) + content = _zip_read(data, markup) + assert "ifc://example.com" in content + + def test_no_description_element_when_empty(self): + data = build_bcf() + markup = next(n for n in _zip_names(data) if n.endswith("markup.bcf")) + content = _zip_read(data, markup) + assert "" not in content + # --------------------------------------------------------------------------- # /bcf service endpoint tests @@ -164,6 +176,13 @@ class TestBcfEndpoint: assert "My issue" in content assert "Fix this" in content + def test_source_url_in_description(self): + r = client.post("/bcf", json={"url": CAMERA_URL}) + markup = next(n for n in _zip_names(r.content) if n.endswith("markup.bcf")) + content = _zip_read(r.content, markup) + # & is XML-escaped to & in the description element + assert CAMERA_URL.replace("&", "&") in content + def test_clip_plane_in_viewpoint(self): r = client.post("/bcf", json={"url": CLIP_URL}) vp = next(n for n in _zip_names(r.content) if n.endswith("viewpoint.bcfv"))