- validate_artifact: add empty content_type -> MissingField; size_bytes <= 0 -> new BadSize; sha256 not exactly 64 lowercase-hex -> new BadDigest (via validate_sha256). The valid fixture (64-hex sha, positive size, non-empty content_type) stays accepted. - Release: rename updated_at -> published_at (published_at = 0 means draft). Final order: id, app_id, version, build, channel, notes, created_by, created_at, published_at. - Artifact: add opaque metadata: string before validation_status. - tests: add empty-content_type/MissingField, bad-size/BadSize, malformed-sha256/BadDigest cases and contract pins reading back Release.published_at and Artifact.metadata. PO-ruled out of scope and NOT added: Token model, in-memory repository (P2.3).
213 lines
7.2 KiB
Plaintext
213 lines
7.2 KiB
Plaintext
// Acceptance for P2.1 — the domain boundary validator. Constructs valid
|
|
// App/Release/Artifact/Channel and asserts they are accepted, then mutates
|
|
// each into one invalid form and asserts it is rejected with the SPECIFIC
|
|
// typed ValidationErr (not merely "some failure"). Prints a pass/fail line
|
|
// per case and exits non-zero if any case behaves unexpectedly.
|
|
#import "modules/std.sx";
|
|
#import "../src/domain/platform.sx";
|
|
#import "../src/domain/app.sx";
|
|
#import "../src/domain/release.sx";
|
|
#import "../src/domain/artifact.sx";
|
|
#import "../src/domain/channel.sx";
|
|
#import "../src/domain/validate.sx";
|
|
|
|
// ── Valid fixtures ──────────────────────────────────────────────────────
|
|
|
|
valid_app :: () -> App {
|
|
a : App = .{
|
|
id = "app_01",
|
|
slug = "acme-app",
|
|
display_name = "Acme App",
|
|
owner = "user_01",
|
|
visibility = .public,
|
|
created_at = 1700000000,
|
|
updated_at = 1700000000,
|
|
};
|
|
a.bundle_ids.append(BundleId.{ platform = .ios, value = "co.acme.app" });
|
|
return a;
|
|
}
|
|
|
|
valid_release :: () -> Release {
|
|
return Release.{
|
|
id = "rel_01",
|
|
app_id = "app_01",
|
|
version = "1.2.3-beta.1",
|
|
build = 42,
|
|
channel = "stable",
|
|
notes = "first cut",
|
|
created_by = "user_01",
|
|
created_at = 1700000000,
|
|
published_at = 1700000200,
|
|
};
|
|
}
|
|
|
|
valid_artifact :: () -> Artifact {
|
|
return Artifact.{
|
|
id = "art_01",
|
|
app_id = "app_01",
|
|
release_id = "rel_01",
|
|
platform = .android_apk,
|
|
filename = "acme.apk",
|
|
content_type = "application/vnd.android.package-archive",
|
|
size_bytes = 10485760,
|
|
sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
storage_key = "apps/app_01/rel_01/acme.apk",
|
|
metadata = "{\"min_os\":\"14\"}",
|
|
validation_status = .valid,
|
|
};
|
|
}
|
|
|
|
valid_channel :: () -> Channel {
|
|
return Channel.{
|
|
app_id = "app_01",
|
|
name = "stable",
|
|
current_release_id = "rel_01",
|
|
policy = .percentage,
|
|
rollout_percent = 25,
|
|
};
|
|
}
|
|
|
|
// ── Cases ───────────────────────────────────────────────────────────────
|
|
|
|
check_accepts_app :: () -> bool {
|
|
a := valid_app();
|
|
ok := true;
|
|
validate_app(a) catch { ok = false; };
|
|
return ok;
|
|
}
|
|
|
|
check_accepts_release :: () -> bool {
|
|
r := valid_release();
|
|
ok := true;
|
|
validate_release(r) catch { ok = false; };
|
|
return ok;
|
|
}
|
|
|
|
check_accepts_artifact :: () -> bool {
|
|
a := valid_artifact();
|
|
ok := true;
|
|
validate_artifact(a) catch { ok = false; };
|
|
return ok;
|
|
}
|
|
|
|
check_accepts_channel :: () -> bool {
|
|
c := valid_channel();
|
|
ok := true;
|
|
validate_channel(c) catch { ok = false; };
|
|
return ok;
|
|
}
|
|
|
|
check_accepts_platform :: () -> bool {
|
|
p, e := parse_platform("android_apk");
|
|
ok := false;
|
|
if !e { ok = (p == .android_apk); }
|
|
return ok;
|
|
}
|
|
|
|
check_rejects_bad_slug :: () -> bool {
|
|
a := valid_app();
|
|
a.slug = "Bad_Slug"; // uppercase + underscore
|
|
matched := false;
|
|
validate_app(a) catch e { matched = (e == error.BadSlug); };
|
|
return matched;
|
|
}
|
|
|
|
check_rejects_empty_version :: () -> bool {
|
|
r := valid_release();
|
|
r.version = "";
|
|
matched := false;
|
|
validate_release(r) catch e { matched = (e == error.EmptyVersion); };
|
|
return matched;
|
|
}
|
|
|
|
check_rejects_bad_version :: () -> bool {
|
|
r := valid_release();
|
|
r.version = "1.2"; // missing PATCH component
|
|
matched := false;
|
|
validate_release(r) catch e { matched = (e == error.BadVersion); };
|
|
return matched;
|
|
}
|
|
|
|
check_rejects_unknown_platform :: () -> bool {
|
|
_, e := parse_platform("plan9");
|
|
return e == error.UnknownPlatform;
|
|
}
|
|
|
|
check_rejects_bad_channel :: () -> bool {
|
|
c := valid_channel();
|
|
c.name = "Bad Channel"; // space + uppercase
|
|
matched := false;
|
|
validate_channel(c) catch e { matched = (e == error.BadChannelName); };
|
|
return matched;
|
|
}
|
|
|
|
check_rejects_empty_content_type :: () -> bool {
|
|
a := valid_artifact();
|
|
a.content_type = ""; // required string cleared
|
|
matched := false;
|
|
validate_artifact(a) catch e { matched = (e == error.MissingField); };
|
|
return matched;
|
|
}
|
|
|
|
check_rejects_bad_size :: () -> bool {
|
|
a := valid_artifact();
|
|
a.size_bytes = -1; // a content-addressed artifact has positive bytes
|
|
matched := false;
|
|
validate_artifact(a) catch e { matched = (e == error.BadSize); };
|
|
return matched;
|
|
}
|
|
|
|
check_rejects_bad_digest :: () -> bool {
|
|
a := valid_artifact();
|
|
a.sha256 = "not-a-sha"; // not 64 lowercase-hex chars
|
|
matched := false;
|
|
validate_artifact(a) catch e { matched = (e == error.BadDigest); };
|
|
return matched;
|
|
}
|
|
|
|
// Contract pins: read back the renamed Release.published_at and the new
|
|
// Artifact.metadata. These references fail to compile against the pre-change
|
|
// structs (Release.updated_at / no Artifact.metadata).
|
|
check_release_published_at :: () -> bool {
|
|
r := valid_release();
|
|
return r.published_at == 1700000200;
|
|
}
|
|
|
|
check_artifact_metadata :: () -> bool {
|
|
a := valid_artifact();
|
|
return a.metadata == "{\"min_os\":\"14\"}";
|
|
}
|
|
|
|
run_case :: (label: string, ok: bool) -> s32 {
|
|
if ok { print(" PASS {}\n", label); return 0; }
|
|
print(" FAIL {}\n", label);
|
|
return 1;
|
|
}
|
|
|
|
main :: () -> s32 {
|
|
failures : s32 = 0;
|
|
failures += run_case("accept: valid app", check_accepts_app());
|
|
failures += run_case("accept: valid release", check_accepts_release());
|
|
failures += run_case("accept: valid artifact", check_accepts_artifact());
|
|
failures += run_case("accept: valid channel", check_accepts_channel());
|
|
failures += run_case("accept: platform 'android_apk'", check_accepts_platform());
|
|
failures += run_case("reject: bad slug -> BadSlug", check_rejects_bad_slug());
|
|
failures += run_case("reject: empty version -> EmptyVersion", check_rejects_empty_version());
|
|
failures += run_case("reject: bad version -> BadVersion", check_rejects_bad_version());
|
|
failures += run_case("reject: unknown platform -> UnknownPlatform", check_rejects_unknown_platform());
|
|
failures += run_case("reject: bad channel -> BadChannelName", check_rejects_bad_channel());
|
|
failures += run_case("reject: empty content_type -> MissingField", check_rejects_empty_content_type());
|
|
failures += run_case("reject: non-positive size_bytes -> BadSize", check_rejects_bad_size());
|
|
failures += run_case("reject: malformed sha256 -> BadDigest", check_rejects_bad_digest());
|
|
failures += run_case("contract: Release.published_at readback", check_release_published_at());
|
|
failures += run_case("contract: Artifact.metadata readback", check_artifact_metadata());
|
|
|
|
print("------------------------------------------------\n");
|
|
if failures == 0 {
|
|
print("domain_validate: ALL CASES PASS\n");
|
|
return 0;
|
|
}
|
|
print("domain_validate: {} CASE(S) FAILED\n", failures);
|
|
return 1;
|
|
}
|