Files
DECNET/tests/canary/test_generators.py
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

212 lines
7.8 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Coverage for the synthesised-artifact generators.
Each generator MUST be deterministic for a given ``CanaryContext`` —
the planter relies on that idempotency to re-seed without storing
the rendered bytes. We assert byte-for-byte stability across two
calls with the same inputs as well as the obvious "slug appears in
the artifact" property.
"""
from __future__ import annotations
import re
import shutil
from pathlib import Path
import pytest
from decnet.canary import CanaryContext, get_generator
from decnet.canary.factory import KNOWN_GENERATORS
# fingerprint_* generators shell out to javascript-obfuscator via Node.
# Skip those parametrized cases when the toolchain isn't installed so a
# bare CI checkout doesn't fail before `npm install` runs.
_NEEDS_NODE = {"fingerprint_html", "fingerprint_svg"}
def _node_toolchain_ready() -> bool:
if shutil.which("node") is None:
return False
canary_dir = Path(__file__).resolve().parents[2] / "decnet" / "canary"
return (canary_dir / "node_modules" / "javascript-obfuscator").is_dir()
def _maybe_skip(name: str) -> None:
if name in _NEEDS_NODE and not _node_toolchain_ready():
pytest.skip(f"{name} requires node + javascript-obfuscator")
def _ctx(**kw) -> CanaryContext:
defaults = dict(
callback_token="abcDEF123-test",
http_base="https://canary.example.test",
dns_zone="canary.example.test",
persona="linux",
)
defaults.update(kw)
return CanaryContext(**defaults)
@pytest.mark.parametrize("name", KNOWN_GENERATORS)
def test_generator_is_deterministic(name: str) -> None:
_maybe_skip(name)
g = get_generator(name)
a = g.generate(_ctx())
b = g.generate(_ctx())
assert a.content == b.content, f"{name} not deterministic"
assert a.generator == name
assert a.instrumenter is None
assert a.mode in (0o600, 0o644)
@pytest.mark.parametrize("name", ["git_config", "env_file", "honeydoc"])
def test_callback_url_embedded(name: str) -> None:
g = get_generator(name)
art = g.generate(_ctx(callback_token="slug-XYZ"))
body = art.content.decode("utf-8")
assert "slug-XYZ" in body, f"{name} did not embed slug"
assert "https://canary.example.test" in body
def test_aws_creds_passive_does_not_embed_url() -> None:
# AWS creds are passive — there's no realistic field to hide a URL
# in. Asserting the absence prevents a regression where a future
# change tries to slip the slug into a comment and breaks realism.
g = get_generator("aws_creds")
art = g.generate(_ctx(callback_token="slug-XYZ"))
body = art.content.decode("utf-8")
assert "https://" not in body
assert "slug-XYZ" not in body
# Access key matches the AKIA[A-Z0-9]{16} shape.
assert re.search(r"AKIA[A-Z0-9]{16}", body)
def test_aws_creds_changes_with_slug() -> None:
g = get_generator("aws_creds")
a = g.generate(_ctx(callback_token="slug-A"))
b = g.generate(_ctx(callback_token="slug-B"))
assert a.content != b.content
def test_ssh_key_uses_dns_zone_when_available() -> None:
g = get_generator("ssh_key")
art = g.generate(_ctx(callback_token="slugZ", dns_zone="canary.test"))
assert b"slugZ.canary.test" in art.content
def test_ssh_key_falls_back_to_http_host_without_dns() -> None:
g = get_generator("ssh_key")
art = g.generate(_ctx(
http_base="https://example.test", dns_zone="",
))
assert b"example.test" in art.content
def test_honeydoc_html_is_valid_ish_html() -> None:
g = get_generator("honeydoc")
art = g.generate(_ctx())
body = art.content.decode("utf-8")
assert "<!DOCTYPE html>" in body
assert "<img" in body
assert "width=\"1\" height=\"1\"" in body
def test_honeydoc_docx_produces_valid_zip_with_callback() -> None:
import io
import zipfile
g = get_generator("honeydoc_docx")
art = g.generate(_ctx(callback_token="slugDX"))
assert art.content[:4] == b"PK\x03\x04" # zip magic
with zipfile.ZipFile(io.BytesIO(art.content), "r") as zf:
names = set(zf.namelist())
assert {"[Content_Types].xml", "_rels/.rels", "word/document.xml",
"word/_rels/document.xml.rels"} <= names
rels = zf.read("word/_rels/document.xml.rels").decode()
assert "https://canary.example.test/c/slugDX" in rels
assert "TargetMode=\"External\"" in rels
doc = zf.read("word/document.xml").decode()
assert "Q3 Operations Review" in doc
assert "<w:drawing>" in doc
def test_honeydoc_pdf_produces_valid_pdf_with_openaction() -> None:
pikepdf = pytest.importorskip("pikepdf")
g = get_generator("honeydoc_pdf")
art = g.generate(_ctx(callback_token="slugPDF"))
assert art.content[:5] == b"%PDF-"
# Re-open and confirm OpenAction URI round-trips.
import io
with pikepdf.open(io.BytesIO(art.content)) as pdf:
action = pdf.Root["/OpenAction"]
assert str(action["/S"]) == "/URI"
assert str(action["/URI"]) == "https://canary.example.test/c/slugPDF"
def test_git_config_remote_url_shape() -> None:
g = get_generator("git_config")
art = g.generate(_ctx(callback_token="slug42"))
body = art.content.decode("utf-8")
assert "[remote \"origin\"]" in body
assert "https://canary.example.test/c/slug42/repo.git" in body
def test_env_file_carries_two_callback_fields() -> None:
g = get_generator("env_file")
art = g.generate(_ctx(callback_token="slugEnv"))
body = art.content.decode("utf-8")
assert "API_BASE_URL=https://canary.example.test/c/slugEnv" in body
assert "WEBHOOK_NOTIFY_URL=https://canary.example.test/c/slugEnv/webhook" in body
def test_mysql_dump_requires_dns_zone() -> None:
g = get_generator("mysql_dump")
with pytest.raises(ValueError, match="dns_zone"):
g.generate(_ctx(dns_zone=""))
def test_mysql_dump_payload_round_trips_through_base64() -> None:
import base64 as _b64
g = get_generator("mysql_dump")
art = g.generate(_ctx(callback_token="slugSQL", dns_zone="canary.test"))
body = art.content.decode("utf-8")
# Slug must NOT appear in plaintext — the camouflage is base64.
assert "slugSQL" not in body.replace("\n", " ").split("SET @b = '")[0]
# Locate the base64 blob and decode it; the inner SQL must reference
# the slug-bearing replica host, smuggle @@hostname/@@lc_time_names
# into SOURCE_USER, and target port 3306.
m = re.search(r"SET @b = '([A-Za-z0-9+/=]+)';", body)
assert m, "expected base64 payload assignment"
inner = _b64.b64decode(m.group(1)).decode("utf-8")
assert "slugSQL.canary.test" in inner
assert "SOURCE_PORT=3306" in inner
assert "@@hostname" in inner
assert "@@lc_time_names" in inner
assert "CHANGE REPLICATION SOURCE TO" in inner
def test_mysql_dump_executes_and_starts_replica() -> None:
g = get_generator("mysql_dump")
art = g.generate(_ctx(callback_token="slugSQL2", dns_zone="canary.test"))
body = art.content.decode("utf-8")
# The PREPARE/EXECUTE/START REPLICA chain is what makes the import
# actually phone home; missing any of these silently breaks the trip.
assert "PREPARE stmt1 FROM @s2;" in body
assert "EXECUTE stmt1;" in body
assert "PREPARE stmt2 FROM @bb;" in body
assert "EXECUTE stmt2;" in body
assert "START REPLICA;" in body
# Realism: header + trailer markers that mysqldump emits.
assert body.startswith("-- MySQL dump")
assert "-- Dump completed" in body
def test_artifacts_carry_notes() -> None:
# Notes drive the API ``preview`` endpoint so operators can sanity-
# check what we did before the file lands. Empty notes would mean
# the operator is staring at opaque bytes.
for name in KNOWN_GENERATORS:
if name in _NEEDS_NODE and not _node_toolchain_ready():
continue
art = get_generator(name).generate(_ctx())
assert art.notes, f"{name} produced no notes"