Files
jobtrackingapp/job-tracker-ui/e2e/helpers/seed-operation.py
T

238 lines
12 KiB
Python

"""Insert one inert queued task into the existing disposable native-test database.
Stdin accepts only dataRoot, ownerId and ownerEmail. No notification, worker,
provider or product endpoint is created. --self-test uses its own temporary DBs.
"""
from __future__ import annotations
import json
import re
import sqlite3
import sys
import tempfile
import uuid
from contextlib import closing, contextmanager
from datetime import datetime, timezone
from pathlib import Path
class SeedDenied(ValueError):
pass
SCHEMA = {
"Id": ("TEXT", 1, 1), "OwnerUserId": ("TEXT", 1, 0),
"TaskType": ("TEXT", 1, 0), "IdempotencyKey": ("TEXT", 1, 0),
"Status": ("TEXT", 1, 0), "Priority": ("INTEGER", 1, 0),
"EntitlementDecision": ("TEXT", 1, 0), "PrivacyPolicy": ("TEXT", 1, 0),
"SubjectType": ("TEXT", 0, 0), "SubjectId": ("TEXT", 0, 0),
"Provider": ("TEXT", 0, 0), "Model": ("TEXT", 0, 0),
"AttemptCount": ("INTEGER", 1, 0), "MaxAttempts": ("INTEGER", 1, 0),
"CreatedAtUtc": ("TEXT", 1, 0), "AvailableAtUtc": ("TEXT", 1, 0),
"StartedAtUtc": ("TEXT", 0, 0), "CompletedAtUtc": ("TEXT", 0, 0),
"DeadlineAtUtc": ("TEXT", 0, 0), "CancellationRequestedAtUtc": ("TEXT", 0, 0),
"LeaseToken": ("TEXT", 0, 0), "LeaseExpiresAtUtc": ("TEXT", 0, 0),
"LastHeartbeatAtUtc": ("TEXT", 0, 0), "ProgressStage": ("TEXT", 0, 0),
"ProgressPercent": ("INTEGER", 0, 0), "FailureCategory": ("TEXT", 0, 0),
"FailureMessage": ("TEXT", 0, 0), "ResultReference": ("TEXT", 0, 0),
}
def insert_only_authorizer():
inserts = 0
def authorize(action, target, column, database, trigger):
nonlocal inserts
if action == sqlite3.SQLITE_INSERT:
inserts += 1
return sqlite3.SQLITE_OK if target == "UserOperations" and inserts == 1 and trigger is None else sqlite3.SQLITE_DENY
if action in {sqlite3.SQLITE_READ, sqlite3.SQLITE_SELECT, sqlite3.SQLITE_TRANSACTION, sqlite3.SQLITE_FUNCTION}:
return sqlite3.SQLITE_OK
if action == sqlite3.SQLITE_PRAGMA and target == "table_info":
return sqlite3.SQLITE_OK
return sqlite3.SQLITE_DENY
return authorize
def seed_operation(payload: object) -> str:
if not isinstance(payload, dict) or set(payload) != {"dataRoot", "ownerId", "ownerEmail"}:
raise SeedDenied("Exactly dataRoot, ownerId and ownerEmail are required")
if any(not isinstance(value, str) or not value.strip() for value in payload.values()):
raise SeedDenied("All inputs must be nonempty strings")
requested = Path(payload["dataRoot"])
if not requested.is_absolute():
raise SeedDenied("dataRoot must be absolute")
try:
root, temporary = requested.resolve(strict=True), Path(tempfile.gettempdir()).resolve(strict=True)
except OSError as error:
raise SeedDenied("The isolated dataRoot must already exist") from error
if (root != requested or root.parent != temporary or not root.is_dir()
or requested.is_symlink() or requested.is_junction()
or not re.fullmatch(r"jobtracker-e2e-[A-Za-z0-9][A-Za-z0-9_-]*", root.name)):
raise SeedDenied("Only an unlinked jobtracker-e2e-* direct child of system temp is allowed")
database = root / "jobtracker.db"
if (not database.is_file() or database.is_symlink() or database.is_junction()
or database.resolve(strict=True) != database or database.stat().st_nlink != 1):
raise SeedDenied("The unlinked existing direct jobtracker.db file is required")
# EF's SQLite Guid converter writes uppercase canonical text.
operation_id = str(uuid.uuid4()).upper()
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")
with closing(sqlite3.connect(database.as_uri() + "?mode=rw", uri=True, timeout=5)) as connection, connection:
connection.execute("PRAGMA foreign_keys=ON")
connection.set_authorizer(insert_only_authorizer())
connection.execute("BEGIN IMMEDIATE")
actual = {row[1]: (row[2].upper(), row[3], row[5]) for row in connection.execute('PRAGMA table_info("UserOperations")')}
if actual != SCHEMA:
raise SeedDenied("UserOperations must match its existing 28-column SQLite migration")
users = {row[1]: row[2].upper() for row in connection.execute('PRAGMA table_info("AspNetUsers")')}
if users.get("Id") != "TEXT" or users.get("Email") != "TEXT":
raise SeedDenied("Existing AspNetUsers Id/Email schema is required")
owner = connection.execute('SELECT "Id", "Email" FROM "AspNetUsers" WHERE "Id" = ?', (payload["ownerId"],)).fetchone()
if owner != (payload["ownerId"], payload["ownerEmail"]):
raise SeedDenied("Owner ID/email must exactly match the existing real fixture account")
if connection.execute("SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='UserOperations' LIMIT 1").fetchone():
raise SeedDenied("UserOperations triggers are outside this insert-only fixture contract")
connection.execute(
'INSERT INTO "UserOperations" ("Id", "OwnerUserId", "TaskType", "IdempotencyKey", "Status", "Priority", "EntitlementDecision", "PrivacyPolicy", "AttemptCount", "MaxAttempts", "CreatedAtUtc", "AvailableAtUtc") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
(operation_id, payload["ownerId"], "native-offline-fixture", uuid.uuid4().hex,
"queued", 0, "native_fixture", "local_only", 0, 3, timestamp, timestamp),
)
return operation_id.lower()
@contextmanager
def temporary_fixture():
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-operation-selftest-") as directory:
root = Path(directory).resolve()
try:
yield root
finally:
assert root.parent == Path(tempfile.gettempdir()).resolve()
assert root.name.startswith("jobtracker-e2e-operation-selftest-") and not root.is_symlink() and not root.is_junction()
def self_test() -> dict:
passed = []
owner, email = "owner' OR 1=1 --", "quote'@example.test"
def fixture(root, variant=None):
database = root / "jobtracker.db"
columns = []
for name, (kind, required, primary) in SCHEMA.items():
if variant == "missing_column" and name == "ResultReference":
continue
if variant == "wrong_type" and name == "Status":
kind = "INTEGER"
if variant == "wrong_nullability" and name == "Status":
required = 0
columns.append(f'"{name}" {kind}' + (" NOT NULL" if required else "") + (" PRIMARY KEY" if primary else ""))
if variant == "extra_column":
columns.append('"Unexpected" TEXT')
with closing(sqlite3.connect(database)) as connection, connection:
connection.execute('CREATE TABLE "UserOperations" (' + ",".join(columns) + ")")
connection.execute('CREATE TABLE "AspNetUsers" ("Id" TEXT PRIMARY KEY, "Email" TEXT, "Canonical" TEXT)')
connection.execute('INSERT INTO "AspNetUsers" VALUES (?, ?, ?)', (owner, email, "Canonical source — æøå"))
connection.execute('CREATE TABLE "UserNotifications" ("Id" TEXT PRIMARY KEY, "Title" TEXT)')
connection.execute('INSERT INTO "UserNotifications" VALUES (?, ?)', ("existing-notice", "Preserve notice"))
return database
def snapshot(database):
with closing(sqlite3.connect(database)) as connection:
return {table: connection.execute(f'SELECT * FROM "{table}" ORDER BY rowid').fetchall()
for table in ("AspNetUsers", "UserOperations", "UserNotifications", "sqlite_master")}
def payload(root, **changes):
return {"dataRoot": str(root), "ownerId": owner, "ownerEmail": email, **changes}
def deny(name, value):
try:
seed_operation(value)
except SeedDenied:
passed.append(name)
else:
raise AssertionError(name + " was accepted")
with temporary_fixture() as root:
deny("missing_database_does_not_create", payload(root))
assert not (root / "jobtracker.db").exists()
database = fixture(root)
before = snapshot(database)
for name, value in (("unknown_key", payload(root, taskType="live")), ("missing_key", {"dataRoot": str(root)}),
("relative_root", payload(root, dataRoot=".")), ("wrong_owner", payload(root, ownerId="other")),
("wrong_email", payload(root, ownerEmail="other@example.test")), ("exact_email_case", payload(root, ownerEmail=email.upper())),
("empty_owner", payload(root, ownerId="")), ("non_object", [])):
deny(name, value)
assert snapshot(database) == before
passed.append("denied_inputs_preserve_rows_and_schema")
identifier = seed_operation(payload(root))
after = snapshot(database)
assert len(after["UserOperations"]) == 1
assert all(after[key] == value for key, value in before.items() if key != "UserOperations")
with closing(sqlite3.connect(database)) as connection:
connection.row_factory = sqlite3.Row
row = dict(connection.execute('SELECT * FROM "UserOperations"').fetchone())
assert row["Id"] == identifier.upper() and str(uuid.UUID(row["Id"])).upper() == row["Id"]
assert row["OwnerUserId"] == owner and row["TaskType"] == "native-offline-fixture"
assert row["Status"] == "queued" and row["EntitlementDecision"] == "native_fixture" and row["PrivacyPolicy"] == "local_only"
assert row["AttemptCount"] == 0 and row["MaxAttempts"] == 3 and row["CreatedAtUtc"] == row["AvailableAtUtc"]
assert all(row[key] is None for key, (_, required, _) in SCHEMA.items() if not required)
passed.extend(("parameterized_exact_insert", "ef_uppercase_guid", "no_subject_provider_model_lease_result", "existing_rows_and_schema_unchanged"))
for statement in ('UPDATE "AspNetUsers" SET "Canonical" = \'lost\'', 'DELETE FROM "UserOperations"',
'INSERT INTO "UserNotifications" VALUES (\'new\', \'wrong\')', 'CREATE TABLE "Forbidden" ("Id" INTEGER)',
"ATTACH DATABASE ':memory:' AS forbidden", "PRAGMA writable_schema=ON"):
with closing(sqlite3.connect(database)) as connection:
connection.set_authorizer(insert_only_authorizer())
try:
connection.execute(statement)
except sqlite3.DatabaseError:
passed.append("authorizer_denies_" + statement.split()[0].lower())
else:
raise AssertionError("Forbidden SQL was accepted: " + statement)
assert snapshot(database) == after
passed.append("denied_sql_preserves_all_rows_and_schema")
(root / "fixture-hardlink.db").hardlink_to(database)
deny("hardlinked_database", payload(root))
assert snapshot(database) == after
for variant in ("missing_column", "extra_column", "wrong_type", "wrong_nullability"):
with temporary_fixture() as root:
database = fixture(root, variant)
before = snapshot(database)
deny(variant, payload(root))
assert snapshot(database) == before
with temporary_fixture() as root:
database = fixture(root)
with closing(sqlite3.connect(database)) as connection, connection:
connection.execute('CREATE TRIGGER "unexpected_write" AFTER INSERT ON "UserOperations" BEGIN UPDATE "AspNetUsers" SET "Canonical" = \'lost\'; END')
before = snapshot(database)
deny("trigger_denied_before_insert", payload(root))
assert snapshot(database) == before
with temporary_fixture() as root:
nested = root / "jobtracker-e2e-nested"
nested.mkdir()
database = fixture(nested)
before = snapshot(database)
deny("nested_root", payload(nested))
deny("wrong_prefix", payload(root, dataRoot=str(root.parent)))
assert snapshot(database) == before
return {"selfTest": "PASS", "checks": len(passed), "passed": passed, "databaseScope": "own direct temporary SQLite fixtures only"}
def main():
try:
if sys.argv[1:] == ["--self-test"]:
print(json.dumps(self_test()))
elif sys.argv[1:]:
raise SeedDenied("Only stdin JSON or --self-test is supported")
else:
print(json.dumps({"operationId": seed_operation(json.load(sys.stdin))}))
return 0
except (SeedDenied, json.JSONDecodeError, sqlite3.Error, OSError) as error:
print(json.dumps({"error": str(error)}), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())