487 lines
19 KiB
Python
487 lines
19 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import copy
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import sqlite3
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from dataclasses import replace
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest import mock
|
||
|
|
|
||
|
|
from docforge.errors import DocForgeError
|
||
|
|
from docforge.manual_projection import (
|
||
|
|
build_manual_projection_package,
|
||
|
|
build_manual_render_plan,
|
||
|
|
)
|
||
|
|
from docforge.models import Edge
|
||
|
|
from docforge.project import Project
|
||
|
|
from docforge.projection_contract import (
|
||
|
|
MANUAL_RENDER_PLAN_CONTRACT,
|
||
|
|
PROJECTION_PACKAGE_CONTRACT,
|
||
|
|
PROJECTION_RECEIPT_CONTRACT,
|
||
|
|
ManualRenderPlanV1,
|
||
|
|
ProjectionArtifact,
|
||
|
|
ProjectionPackageV1,
|
||
|
|
ProjectionReceiptV1,
|
||
|
|
canonical_projection_bytes,
|
||
|
|
projection_hash,
|
||
|
|
)
|
||
|
|
from docforge.render_contract import GenericHtmlRenderer
|
||
|
|
from docforge_renderers.manual import ManualHtmlRenderer
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
FIXTURES = ROOT / "tests" / "fixtures"
|
||
|
|
|
||
|
|
ALPHA_RENDERER_VERSION = "1+markdown-it-py-4.2.0"
|
||
|
|
ALPHA_RENDER_IDENTITY = "1c0a49c28ba3b0dabf94be36e75def197dee1be3cb73ac405b09875383c8dc5f"
|
||
|
|
ALPHA_OUTPUT_HASH = "81656bb89debc7ad1fbe8bc290e9a3ba90664442b17a6d57e908d30d20c47f77"
|
||
|
|
ALPHA_OUTPUT_BYTES = 2043
|
||
|
|
|
||
|
|
|
||
|
|
class ProjectionContractTests(unittest.TestCase):
|
||
|
|
def setUp(self) -> None:
|
||
|
|
self.temporary = tempfile.TemporaryDirectory()
|
||
|
|
self.addCleanup(self.temporary.cleanup)
|
||
|
|
self.root = Path(self.temporary.name) / "alpha"
|
||
|
|
shutil.copytree(FIXTURES / "alpha", self.root)
|
||
|
|
self.project = Project.open(self.root)
|
||
|
|
self.snapshot = self.project.load()
|
||
|
|
assert self.snapshot.descriptor.render is not None
|
||
|
|
self.view = self.snapshot.descriptor.render.views[0]
|
||
|
|
self.template = self.view.template_path.read_bytes()
|
||
|
|
self.plan = build_manual_render_plan(
|
||
|
|
self.snapshot,
|
||
|
|
self.view,
|
||
|
|
changeset_hash=None,
|
||
|
|
)
|
||
|
|
self.package = build_manual_projection_package(
|
||
|
|
self.plan,
|
||
|
|
self.template,
|
||
|
|
renderer_id="generic_html",
|
||
|
|
renderer_version=ALPHA_RENDERER_VERSION,
|
||
|
|
max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes,
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_canonical_identity_is_stable_and_tampering_is_rejected(self) -> None:
|
||
|
|
self.assertEqual(
|
||
|
|
b'{"a":"\xc3\xa9","b":1}',
|
||
|
|
canonical_projection_bytes({"b": 1, "a": "\N{LATIN SMALL LETTER E WITH ACUTE}"}),
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
projection_hash(
|
||
|
|
{key: value for key, value in self.plan.as_dict().items() if key != "plan_id"}
|
||
|
|
),
|
||
|
|
self.plan.plan_id,
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
self.plan.plan_id,
|
||
|
|
build_manual_render_plan(
|
||
|
|
self.snapshot,
|
||
|
|
self.view,
|
||
|
|
changeset_hash=None,
|
||
|
|
).plan_id,
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
self.package.package_id,
|
||
|
|
ProjectionPackageV1.from_dict(
|
||
|
|
json.loads(json.dumps(self.package.as_dict()))
|
||
|
|
).package_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
tampered_plan = copy.deepcopy(self.plan.as_dict())
|
||
|
|
pages = tampered_plan["pages"]
|
||
|
|
assert isinstance(pages, list)
|
||
|
|
assert isinstance(pages[0], dict)
|
||
|
|
pages[0]["title"] = "Tampered title"
|
||
|
|
with self.assertRaises(DocForgeError) as plan_error:
|
||
|
|
ManualRenderPlanV1.from_dict(tampered_plan)
|
||
|
|
self.assertEqual("invalid_projection", plan_error.exception.code)
|
||
|
|
|
||
|
|
tampered_package = copy.deepcopy(self.package.as_dict())
|
||
|
|
assets = tampered_package["assets"]
|
||
|
|
assert isinstance(assets, list)
|
||
|
|
assert isinstance(assets[0], dict)
|
||
|
|
assets[0]["text"] = f"{assets[0]['text']}\nTampered"
|
||
|
|
with self.assertRaises(DocForgeError) as package_error:
|
||
|
|
ProjectionPackageV1.from_dict(tampered_package)
|
||
|
|
self.assertEqual("invalid_projection", package_error.exception.code)
|
||
|
|
|
||
|
|
def test_contract_documents_reject_unknown_or_malformed_fields(self) -> None:
|
||
|
|
plan_with_extra = copy.deepcopy(self.plan.as_dict())
|
||
|
|
plan_with_extra["unexpected"] = True
|
||
|
|
with self.assertRaises(DocForgeError) as extra_plan:
|
||
|
|
ManualRenderPlanV1.from_dict(plan_with_extra)
|
||
|
|
self.assertEqual("invalid_projection", extra_plan.exception.code)
|
||
|
|
|
||
|
|
plan_with_foreign_project = copy.deepcopy(self.plan.as_dict())
|
||
|
|
project = plan_with_foreign_project["project"]
|
||
|
|
assert isinstance(project, dict)
|
||
|
|
project["absolute_root"] = str(self.root)
|
||
|
|
with self.assertRaises(DocForgeError) as foreign_project:
|
||
|
|
ManualRenderPlanV1.from_dict(plan_with_foreign_project)
|
||
|
|
self.assertEqual("invalid_projection", foreign_project.exception.code)
|
||
|
|
|
||
|
|
package_with_extra = copy.deepcopy(self.package.as_dict())
|
||
|
|
package_with_extra["unexpected"] = []
|
||
|
|
with self.assertRaises(DocForgeError) as extra_package:
|
||
|
|
ProjectionPackageV1.from_dict(package_with_extra)
|
||
|
|
self.assertEqual("invalid_projection", extra_package.exception.code)
|
||
|
|
|
||
|
|
result = ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(self.package)
|
||
|
|
receipt_with_extra = copy.deepcopy(result.receipt.as_dict())
|
||
|
|
receipt_with_extra["artifact_bytes"] = "forbidden"
|
||
|
|
with self.assertRaises(DocForgeError) as extra_receipt:
|
||
|
|
ProjectionReceiptV1.from_dict(receipt_with_extra)
|
||
|
|
self.assertEqual("invalid_projection", extra_receipt.exception.code)
|
||
|
|
|
||
|
|
with self.assertRaises(DocForgeError) as path_artifact:
|
||
|
|
ProjectionReceiptV1.create(
|
||
|
|
kind="manual",
|
||
|
|
package_id=self.package.package_id,
|
||
|
|
plan_id=self.plan.plan_id,
|
||
|
|
renderer={
|
||
|
|
"renderer_id": "generic_html",
|
||
|
|
"renderer_version": ALPHA_RENDERER_VERSION,
|
||
|
|
},
|
||
|
|
artifacts=[
|
||
|
|
{
|
||
|
|
"artifact_id": "../manual.html",
|
||
|
|
"media_type": "text/html",
|
||
|
|
"sha256": "0" * 64,
|
||
|
|
"bytes": 1,
|
||
|
|
}
|
||
|
|
],
|
||
|
|
diagnostics={},
|
||
|
|
timing={"elapsed_ns": 0},
|
||
|
|
peak_memory_bytes=None,
|
||
|
|
)
|
||
|
|
self.assertEqual("invalid_projection", path_artifact.exception.code)
|
||
|
|
|
||
|
|
def test_projection_package_is_path_free_and_rejects_runtime_references(self) -> None:
|
||
|
|
serialized = canonical_projection_bytes(self.package.as_dict())
|
||
|
|
self.assertNotIn(str(self.root).encode("utf-8"), serialized)
|
||
|
|
self.assertNotIn(b"source_path", serialized)
|
||
|
|
self.assertNotIn(b"sqlite", serialized.lower())
|
||
|
|
|
||
|
|
with self.assertRaises(DocForgeError) as absolute_path:
|
||
|
|
ProjectionPackageV1.create(
|
||
|
|
kind="manual",
|
||
|
|
plan=self.plan,
|
||
|
|
renderer={
|
||
|
|
"renderer_id": "generic_html",
|
||
|
|
"renderer_version": ALPHA_RENDERER_VERSION,
|
||
|
|
},
|
||
|
|
components=[],
|
||
|
|
assets=[],
|
||
|
|
output_policy={
|
||
|
|
"artifact_ids": ["manual.html"],
|
||
|
|
"max_total_bytes": 1000,
|
||
|
|
"template_path": "/home/example/private-template.html",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
self.assertEqual("invalid_projection", absolute_path.exception.code)
|
||
|
|
|
||
|
|
with self.assertRaises(DocForgeError) as database_reference:
|
||
|
|
ProjectionPackageV1.create(
|
||
|
|
kind="manual",
|
||
|
|
plan=self.plan,
|
||
|
|
renderer={
|
||
|
|
"renderer_id": "generic_html",
|
||
|
|
"renderer_version": ALPHA_RENDERER_VERSION,
|
||
|
|
},
|
||
|
|
components=[],
|
||
|
|
assets=[],
|
||
|
|
output_policy={
|
||
|
|
"artifact_ids": ["manual.html"],
|
||
|
|
"max_total_bytes": 1000,
|
||
|
|
"database": "index.sqlite3",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
self.assertEqual("invalid_projection", database_reference.exception.code)
|
||
|
|
|
||
|
|
def test_receipt_attests_artifacts_without_embedding_content(self) -> None:
|
||
|
|
result = ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(
|
||
|
|
self.package,
|
||
|
|
render_identity=ALPHA_RENDER_IDENTITY,
|
||
|
|
)
|
||
|
|
self.assertEqual(1, len(result.artifacts))
|
||
|
|
artifact = result.artifacts[0]
|
||
|
|
evidence = artifact.evidence()
|
||
|
|
receipt = result.receipt.as_dict()
|
||
|
|
|
||
|
|
self.assertEqual(self.package.package_id, receipt["package_id"])
|
||
|
|
self.assertEqual(self.plan.plan_id, receipt["plan_id"])
|
||
|
|
self.assertEqual([evidence], receipt["artifacts"])
|
||
|
|
self.assertEqual(PROJECTION_RECEIPT_CONTRACT, receipt["contract"])
|
||
|
|
self.assertEqual(
|
||
|
|
{
|
||
|
|
"renderer_id": "generic_html",
|
||
|
|
"renderer_version": ALPHA_RENDERER_VERSION,
|
||
|
|
},
|
||
|
|
receipt["renderer"],
|
||
|
|
)
|
||
|
|
self.assertEqual({"warnings": []}, receipt["diagnostics"])
|
||
|
|
self.assertIsNone(receipt["peak_memory_bytes"])
|
||
|
|
timing = receipt["timing"]
|
||
|
|
assert isinstance(timing, dict)
|
||
|
|
self.assertGreaterEqual(timing["elapsed_ns"], 0)
|
||
|
|
self.assertNotIn("content", evidence)
|
||
|
|
self.assertNotIn(artifact.content, canonical_projection_bytes(receipt))
|
||
|
|
self.assertEqual(
|
||
|
|
receipt["receipt_id"],
|
||
|
|
ProjectionReceiptV1.from_dict(copy.deepcopy(receipt)).receipt_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
tampered_receipt = copy.deepcopy(receipt)
|
||
|
|
artifacts = tampered_receipt["artifacts"]
|
||
|
|
assert isinstance(artifacts, list)
|
||
|
|
assert isinstance(artifacts[0], dict)
|
||
|
|
artifacts[0]["bytes"] = int(artifacts[0]["bytes"]) + 1
|
||
|
|
with self.assertRaises(DocForgeError) as tampered:
|
||
|
|
ProjectionReceiptV1.from_dict(tampered_receipt)
|
||
|
|
self.assertEqual("invalid_projection", tampered.exception.code)
|
||
|
|
|
||
|
|
def test_manual_plan_is_deterministic_and_preserves_alpha_semantics(self) -> None:
|
||
|
|
document = self.plan.as_dict()
|
||
|
|
self.assertEqual(MANUAL_RENDER_PLAN_CONTRACT, document["contract"])
|
||
|
|
self.assertIsNone(document["changeset_hash"])
|
||
|
|
pages = document["pages"]
|
||
|
|
navigation = document["navigation"]
|
||
|
|
search_documents = document["search_documents"]
|
||
|
|
diagnostics = document["diagnostics"]
|
||
|
|
assert isinstance(pages, list)
|
||
|
|
assert isinstance(navigation, list)
|
||
|
|
assert isinstance(search_documents, list)
|
||
|
|
assert isinstance(diagnostics, dict)
|
||
|
|
|
||
|
|
self.assertEqual(
|
||
|
|
["guide.foundation", "guide.workflow", "proof.validation"],
|
||
|
|
[page["node_id"] for page in pages],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
["guide.foundation", "guide.workflow", "proof.validation"],
|
||
|
|
[item["node_id"] for item in navigation],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
["guide.foundation", "guide.workflow", "proof.validation"],
|
||
|
|
[item["node_id"] for item in search_documents],
|
||
|
|
)
|
||
|
|
self.assertEqual([], diagnostics["orphans"])
|
||
|
|
self.assertEqual([], diagnostics["cycles"])
|
||
|
|
|
||
|
|
page_by_id = {page["node_id"]: page for page in pages}
|
||
|
|
self.assertEqual(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"source_id": "guide.workflow",
|
||
|
|
"relation": "depends_on",
|
||
|
|
"target_id": "guide.foundation",
|
||
|
|
}
|
||
|
|
],
|
||
|
|
page_by_id["guide.foundation"]["backlinks"],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"source_id": "guide.workflow",
|
||
|
|
"relation": "depends_on",
|
||
|
|
"target_id": "guide.foundation",
|
||
|
|
}
|
||
|
|
],
|
||
|
|
page_by_id["guide.workflow"]["cross_references"],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"source_id": "proof.validation",
|
||
|
|
"relation": "proves",
|
||
|
|
"target_id": "guide.workflow",
|
||
|
|
}
|
||
|
|
],
|
||
|
|
page_by_id["guide.workflow"]["backlinks"],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"source_id": "proof.validation",
|
||
|
|
"relation": "proves",
|
||
|
|
"target_id": "guide.workflow",
|
||
|
|
}
|
||
|
|
],
|
||
|
|
page_by_id["proof.validation"]["cross_references"],
|
||
|
|
)
|
||
|
|
self.assertTrue(
|
||
|
|
all(
|
||
|
|
page["components"]
|
||
|
|
== [
|
||
|
|
"manual.node-metadata@1",
|
||
|
|
"manual.summary@1",
|
||
|
|
"manual.commonmark@1",
|
||
|
|
"manual.relationships@1",
|
||
|
|
]
|
||
|
|
for page in pages
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
proposed = build_manual_render_plan(
|
||
|
|
self.snapshot,
|
||
|
|
self.view,
|
||
|
|
changeset_hash="a" * 64,
|
||
|
|
)
|
||
|
|
self.assertNotEqual(self.plan.plan_id, proposed.plan_id)
|
||
|
|
self.assertEqual("a" * 64, proposed.as_dict()["changeset_hash"])
|
||
|
|
|
||
|
|
def test_cycle_orphan_backlink_and_cross_reference_planning(self) -> None:
|
||
|
|
edges = (
|
||
|
|
Edge("guide.foundation", "relates_to", "guide.workflow"),
|
||
|
|
Edge("guide.workflow", "returns_to", "guide.foundation"),
|
||
|
|
)
|
||
|
|
snapshot = replace(self.snapshot, edges=edges)
|
||
|
|
first = build_manual_render_plan(snapshot, self.view, changeset_hash=None)
|
||
|
|
second = build_manual_render_plan(snapshot, self.view, changeset_hash=None)
|
||
|
|
self.assertEqual(first.plan_id, second.plan_id)
|
||
|
|
|
||
|
|
document = first.as_dict()
|
||
|
|
diagnostics = document["diagnostics"]
|
||
|
|
pages = document["pages"]
|
||
|
|
assert isinstance(diagnostics, dict)
|
||
|
|
assert isinstance(pages, list)
|
||
|
|
self.assertEqual(["proof.validation"], diagnostics["orphans"])
|
||
|
|
self.assertEqual(
|
||
|
|
[["guide.foundation", "guide.workflow"]],
|
||
|
|
diagnostics["cycles"],
|
||
|
|
)
|
||
|
|
|
||
|
|
page_by_id = {page["node_id"]: page for page in pages}
|
||
|
|
foundation = page_by_id["guide.foundation"]
|
||
|
|
workflow = page_by_id["guide.workflow"]
|
||
|
|
self.assertEqual(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"source_id": "guide.foundation",
|
||
|
|
"relation": "relates_to",
|
||
|
|
"target_id": "guide.workflow",
|
||
|
|
}
|
||
|
|
],
|
||
|
|
foundation["cross_references"],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"source_id": "guide.workflow",
|
||
|
|
"relation": "returns_to",
|
||
|
|
"target_id": "guide.foundation",
|
||
|
|
}
|
||
|
|
],
|
||
|
|
foundation["backlinks"],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
foundation["cross_references"],
|
||
|
|
workflow["backlinks"],
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
foundation["backlinks"],
|
||
|
|
workflow["cross_references"],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_alpha_compatibility_shim_preserves_legacy_identity_and_bytes(self) -> None:
|
||
|
|
renderer = GenericHtmlRenderer()
|
||
|
|
self.assertEqual(ALPHA_RENDERER_VERSION, renderer.renderer_version)
|
||
|
|
prepared = renderer.prepare(
|
||
|
|
self.snapshot,
|
||
|
|
self.view,
|
||
|
|
self.template,
|
||
|
|
changeset_hash=None,
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertEqual(ALPHA_RENDER_IDENTITY, prepared.render_identity)
|
||
|
|
self.assertEqual(ALPHA_OUTPUT_HASH, prepared.output_hash)
|
||
|
|
self.assertEqual(ALPHA_OUTPUT_BYTES, len(prepared.output))
|
||
|
|
self.assertEqual(
|
||
|
|
ALPHA_OUTPUT_HASH,
|
||
|
|
hashlib.sha256(prepared.output).hexdigest(),
|
||
|
|
)
|
||
|
|
self.assertEqual(b"<!DOCTYPE html>", prepared.output.splitlines()[0])
|
||
|
|
self.assertTrue(prepared.output.endswith(b"</html>\n"))
|
||
|
|
self.assertIn(
|
||
|
|
f'content="{ALPHA_RENDER_IDENTITY}"'.encode(),
|
||
|
|
prepared.output,
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_manual_renderer_rejects_project_provided_active_content(self) -> None:
|
||
|
|
for active in (
|
||
|
|
"<script>alert(1)</script>{{ docforge_content }}",
|
||
|
|
'<main onclick="alert(1)">{{ docforge_content }}</main>',
|
||
|
|
'<a href="javascript:alert(1)">{{ docforge_content }}</a>',
|
||
|
|
'<iframe src="https://example.invalid"></iframe>{{ docforge_content }}',
|
||
|
|
'<meta http-equiv="refresh" content="0">{{ docforge_content }}',
|
||
|
|
):
|
||
|
|
with self.subTest(active=active):
|
||
|
|
package = build_manual_projection_package(
|
||
|
|
self.plan,
|
||
|
|
active.encode("utf-8"),
|
||
|
|
renderer_id="generic_html",
|
||
|
|
renderer_version=ALPHA_RENDERER_VERSION,
|
||
|
|
max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes,
|
||
|
|
)
|
||
|
|
with self.assertRaises(DocForgeError) as rejected:
|
||
|
|
ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(package)
|
||
|
|
self.assertEqual("invalid_template", rejected.exception.code)
|
||
|
|
|
||
|
|
def test_manual_renderer_has_no_project_sqlite_or_path_write_capability(self) -> None:
|
||
|
|
renderer = ManualHtmlRenderer(ALPHA_RENDERER_VERSION)
|
||
|
|
forbidden = AssertionError("manual renderer crossed its capability boundary")
|
||
|
|
with (
|
||
|
|
mock.patch.object(Project, "open", side_effect=forbidden),
|
||
|
|
mock.patch.object(Project, "load", side_effect=forbidden),
|
||
|
|
mock.patch.object(sqlite3, "connect", side_effect=forbidden),
|
||
|
|
mock.patch.object(Path, "write_bytes", side_effect=forbidden),
|
||
|
|
mock.patch.object(Path, "write_text", side_effect=forbidden),
|
||
|
|
mock.patch.object(Path, "mkdir", side_effect=forbidden),
|
||
|
|
mock.patch.object(Path, "touch", side_effect=forbidden),
|
||
|
|
mock.patch.object(Path, "unlink", side_effect=forbidden),
|
||
|
|
mock.patch.object(Path, "rename", side_effect=forbidden),
|
||
|
|
mock.patch.object(Path, "replace", side_effect=forbidden),
|
||
|
|
mock.patch.object(os, "mkdir", side_effect=forbidden),
|
||
|
|
mock.patch.object(os, "makedirs", side_effect=forbidden),
|
||
|
|
mock.patch.object(os, "rename", side_effect=forbidden),
|
||
|
|
mock.patch.object(os, "replace", side_effect=forbidden),
|
||
|
|
mock.patch.object(os, "unlink", side_effect=forbidden),
|
||
|
|
):
|
||
|
|
result = renderer.render(
|
||
|
|
self.package,
|
||
|
|
render_identity=ALPHA_RENDER_IDENTITY,
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertEqual(1, len(result.artifacts))
|
||
|
|
self.assertEqual("manual.html", result.artifacts[0].artifact_id)
|
||
|
|
self.assertEqual(ALPHA_OUTPUT_HASH, result.artifacts[0].evidence()["sha256"])
|
||
|
|
|
||
|
|
def test_projection_artifact_evidence_is_canonical_and_content_free(self) -> None:
|
||
|
|
artifact = ProjectionArtifact(
|
||
|
|
artifact_id="manual.html",
|
||
|
|
media_type="text/html; charset=utf-8",
|
||
|
|
content=b"manual bytes",
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
{
|
||
|
|
"artifact_id": "manual.html",
|
||
|
|
"media_type": "text/html; charset=utf-8",
|
||
|
|
"sha256": hashlib.sha256(b"manual bytes").hexdigest(),
|
||
|
|
"bytes": len(b"manual bytes"),
|
||
|
|
},
|
||
|
|
artifact.evidence(),
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
PROJECTION_PACKAGE_CONTRACT,
|
||
|
|
self.package.as_dict()["contract"],
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|