The amalgamation and the bindings now ship with sx itself (sx library/vendors/sqlite/ — bindings + c/ amalgamation); every import flips from ../src/db/sqlite.sx to vendors/sqlite/sqlite.sx, resolved through the compiler's stdlib search paths. vendor/ and src/db/ leave this repo entirely. make test 22/22 — the object cache keys on content, not path, so the relocated source still hits the existing cache entries.
294 lines
15 KiB
Plaintext
294 lines
15 KiB
Plaintext
// Pinned acceptance for P4.4 — distd's token-gated write surface.
|
|
//
|
|
// Mints tokens via the CLI (a publish-scoped one, a read-only one, an
|
|
// app-scoped one for the WRONG app, and a revoked one), starts the BUILT
|
|
// `build/dist server run`, and asserts over curl:
|
|
//
|
|
// * auth: 401 auth.missing / auth.unknown_token without a usable
|
|
// bearer; 403 auth.missing_scope (read token),
|
|
// auth.revoked (revoked token), auth.app_forbidden
|
|
// (token scoped to another app); only tokens that PASSED
|
|
// auth get last_used_at stamped.
|
|
// * upload: POST /api/upload stores the body content-addressed — the
|
|
// returned sha256 equals an independently computed digest of
|
|
// the fixture, the object lands under objects/<sha>, and a
|
|
// re-upload answers the same key (dedup). A bodyless POST is
|
|
// 411.
|
|
// * release: POST /api/apps/<slug>/releases over the uploaded digest
|
|
// publishes through the shared pipeline — visible on the
|
|
// next GET (per-request reload), channel moved; an unknown
|
|
// digest is 404 api.unknown_object and leaves no channel
|
|
// behind; re-publishing the same version is 409.
|
|
// * channel: promote / rollback move the pointer exactly like their
|
|
// CLI twins; unknown release id is 404.
|
|
// * methods: anything but GET/POST is 405.
|
|
//
|
|
// Store-side state is asserted by QUERYING `<store>/dist.db` through the
|
|
// SQLite bindings.
|
|
#import "modules/std.sx";
|
|
#import "modules/std/json.sx";
|
|
process :: #import "modules/std/process.sx";
|
|
fs :: #import "modules/std/fs.sx";
|
|
hash :: #import "modules/std/hash.sx";
|
|
sq :: #import "vendors/sqlite/sqlite.sx";
|
|
|
|
STORE :: ".sx-tmp/server_write";
|
|
PORT :: "18793";
|
|
BASE :: "http://127.0.0.1:18793";
|
|
FIXTURE :: "examples/fixtures/acme-1.2.3-android.apk";
|
|
|
|
get :: (o: Object, key: string) -> Value {
|
|
i := 0;
|
|
while i < o.len {
|
|
if o.items[i].key == key { return o.items[i].val; }
|
|
i += 1;
|
|
}
|
|
process.assert(false, concat("missing json key: ", key));
|
|
dummy : Value = .null_;
|
|
return dummy;
|
|
}
|
|
get_str :: (o: Object, key: string) -> string { return get(o, key).str; }
|
|
get_int :: (o: Object, key: string) -> i64 { return get(o, key).int_; }
|
|
get_obj :: (o: Object, key: string) -> Object { return get(o, key).object; }
|
|
get_arr :: (o: Object, key: string) -> Array { return get(o, key).array; }
|
|
|
|
parse_body :: (body: string, what: string, scratch: Allocator) -> Object {
|
|
v, e := parse(body, scratch);
|
|
if e {
|
|
process.assert(false, concat("response must be valid JSON: ", what));
|
|
dummy : Object = .{};
|
|
return dummy;
|
|
}
|
|
return v.object;
|
|
}
|
|
|
|
// Mint a token via the CLI and return its raw secret.
|
|
mint :: (flags: string, scratch: Allocator) -> string {
|
|
cmd := concat("build/dist token create --local-store .sx-tmp/server_write ", flags);
|
|
cmd = concat(cmd, " --json 2>/dev/null");
|
|
r := process.run(cmd);
|
|
process.assert(r != null and r!.exit_code == 0, concat("token create must exit 0: ", flags));
|
|
o := parse_body(r!.stdout, "token create", scratch);
|
|
return get_str(get_obj(o, "token"), "secret");
|
|
}
|
|
|
|
// POST `path` with optional bearer + data flags; returns the body.
|
|
post :: (path: string, auth: string, dataflags: string) -> string {
|
|
cmd := "curl -s -m 4 -X POST ";
|
|
if auth.len > 0 {
|
|
cmd = concat(cmd, concat("-H 'Authorization: Bearer ", concat(auth, "' ")));
|
|
}
|
|
cmd = concat(cmd, concat(dataflags, concat(" ", concat(BASE, path))));
|
|
r := process.run(cmd);
|
|
process.assert(r != null, concat("curl spawn failed: ", path));
|
|
return r!.stdout;
|
|
}
|
|
|
|
// POST and return only the HTTP status code as text.
|
|
post_code :: (path: string, auth: string, dataflags: string) -> string {
|
|
cmd := "curl -s -m 4 -o /dev/null -w '%{http_code}' -X POST ";
|
|
if auth.len > 0 {
|
|
cmd = concat(cmd, concat("-H 'Authorization: Bearer ", concat(auth, "' ")));
|
|
}
|
|
cmd = concat(cmd, concat(dataflags, concat(" ", concat(BASE, path))));
|
|
r := process.run(cmd);
|
|
process.assert(r != null, concat("curl spawn failed: ", path));
|
|
return r!.stdout;
|
|
}
|
|
|
|
fetch :: (path: string) -> string {
|
|
r := process.run(concat(concat("curl -s -m 4 ", BASE), path));
|
|
process.assert(r != null, concat("curl spawn failed: ", path));
|
|
return r!.stdout;
|
|
}
|
|
|
|
// The error.code member of a JSON error response.
|
|
err_code :: (body: string, what: string, scratch: Allocator) -> string {
|
|
return get_str(get_obj(parse_body(body, what, scratch), "error"), "code");
|
|
}
|
|
|
|
// Open the store database read-only (asserts it exists and opens).
|
|
db_open_ro :: () -> sq.Sqlite {
|
|
c, oe := sq.Sqlite.open_v2(path_join(STORE, "dist.db"), sq.SQLITE_OPEN_READONLY);
|
|
process.assert(!oe, "dist.db must open as a SQLite database");
|
|
c.busy_timeout(2000);
|
|
return c;
|
|
}
|
|
|
|
// current_release_id of channel `name`, "" when the channel doesn't exist.
|
|
channel_pointer :: (name: string) -> string {
|
|
c := db_open_ro();
|
|
st, pe := c.prepare("SELECT current_release_id FROM channels WHERE name = ?1");
|
|
process.assert(!pe, "channel query must prepare");
|
|
st.bind_text(1, name) catch { process.assert(false, "channel bind failed"); };
|
|
rc, se := st.step();
|
|
process.assert(!se, "channel query must step");
|
|
out := "";
|
|
if rc == sq.SQLITE_ROW { out = st.column_text(0); }
|
|
st.finalize();
|
|
c.close();
|
|
return out;
|
|
}
|
|
|
|
// last_used_at of the token named `name` (asserts the token exists).
|
|
token_last_used :: (name: string) -> i64 {
|
|
c := db_open_ro();
|
|
st, pe := c.prepare("SELECT last_used_at FROM tokens WHERE name = ?1");
|
|
process.assert(!pe, "token query must prepare");
|
|
st.bind_text(1, name) catch { process.assert(false, "token bind failed"); };
|
|
rc, se := st.step();
|
|
process.assert(!se, "token query must step");
|
|
process.assert(rc == sq.SQLITE_ROW, concat("token not in the store: ", name));
|
|
out := st.column_int64(0);
|
|
st.finalize();
|
|
c.close();
|
|
return out;
|
|
}
|
|
|
|
// JSON body for a release POST referencing `sha` (single android artifact).
|
|
release_body :: (version: string, channel: string, sha: string) -> string {
|
|
b := concat("-d '{\"version\":\"", version);
|
|
b = concat(b, concat("\",\"channel\":\"", channel));
|
|
b = concat(b, "\",\"artifacts\":[{\"platform\":\"android_apk\",\"sha256\":\"");
|
|
b = concat(b, sha);
|
|
return concat(b, "\"}]}'");
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
gpa := GPA.init();
|
|
arena := Arena.init(xx gpa, 1 << 20);
|
|
defer arena.deinit();
|
|
|
|
process.run("pkill -f 'dist server run --local-store .sx-tmp/server_write' 2>/dev/null");
|
|
process.run(concat("rm -rf ", STORE));
|
|
|
|
// ── tokens (this also creates dist.db on the fresh store) ─────────
|
|
publisher := mint("--name publisher", xx arena);
|
|
reader := mint("--name reader --scope read", xx arena);
|
|
wrong_app := mint("--name wrong-app --app other-app", xx arena);
|
|
revoked := mint("--name doomed", xx arena);
|
|
// revoke "doomed" by id, looked up via token list
|
|
tl := process.run("build/dist token list --local-store .sx-tmp/server_write --json 2>/dev/null");
|
|
process.assert(tl != null and tl!.exit_code == 0, "token list must exit 0");
|
|
toks := get_arr(parse_body(tl!.stdout, "token list", xx arena), "tokens");
|
|
doomed_id := "";
|
|
ti := 0;
|
|
while ti < toks.len {
|
|
to := toks.items[ti].object;
|
|
if get_str(to, "name") == "doomed" { doomed_id = get_str(to, "id"); }
|
|
ti += 1;
|
|
}
|
|
process.assert(doomed_id.len > 0, "doomed token must be listed");
|
|
rv := process.run(concat(concat("build/dist token revoke --id ", doomed_id), " --local-store .sx-tmp/server_write --json 2>/dev/null"));
|
|
process.assert(rv != null and rv!.exit_code == 0, "token revoke must exit 0");
|
|
|
|
// ── server up ──────────────────────────────────────────────────────
|
|
sp := process.run(concat(concat(concat("sh -c 'build/dist server run --local-store ", STORE), concat(concat(" --port ", PORT), " >/dev/null 2>&1 & echo $!'")), ""));
|
|
process.assert(sp != null, "server spawn failed");
|
|
pid := sp!.stdout;
|
|
|
|
ready := false;
|
|
tries := 0;
|
|
while tries < 50 {
|
|
c := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code}' ", BASE), "/healthz"));
|
|
if c != null {
|
|
if c!.stdout == "200" { ready = true; break; }
|
|
}
|
|
process.run("sleep 0.2");
|
|
tries += 1;
|
|
}
|
|
process.assert(ready, "server must answer /healthz within 10s");
|
|
print(" server up\n");
|
|
|
|
upload_data := concat("--data-binary @", FIXTURE);
|
|
|
|
// ── auth refusals ──────────────────────────────────────────────────
|
|
process.assert(post_code("/api/upload", "", upload_data) == "401", "no auth is 401");
|
|
process.assert(err_code(post("/api/upload", "", upload_data), "no-auth body", xx arena) == "auth.missing",
|
|
"no auth names auth.missing");
|
|
process.assert(err_code(post("/api/upload", "dist_bogus", upload_data), "bad-token body", xx arena) == "auth.unknown_token",
|
|
"unknown secret names auth.unknown_token");
|
|
process.assert(post_code("/api/upload", "dist_bogus", upload_data) == "401", "unknown secret is 401");
|
|
process.assert(post_code("/api/upload", reader, upload_data) == "403", "read scope is 403");
|
|
process.assert(err_code(post("/api/upload", reader, upload_data), "reader body", xx arena) == "auth.missing_scope",
|
|
"read scope names auth.missing_scope");
|
|
process.assert(post_code("/api/upload", revoked, upload_data) == "403", "revoked token is 403");
|
|
process.assert(err_code(post("/api/upload", revoked, upload_data), "revoked body", xx arena) == "auth.revoked",
|
|
"revoked token names auth.revoked");
|
|
print(" auth refusals: 401/403 with precise codes\n");
|
|
|
|
// ── upload ─────────────────────────────────────────────────────────
|
|
fixture_bytes := fs.read_file(FIXTURE);
|
|
process.assert(fixture_bytes != null, "fixture must be readable");
|
|
d := hash.sha256_hex(fixture_bytes!);
|
|
expect_sha := string.{ ptr = @d[0], len = 64 };
|
|
|
|
up := parse_body(post("/api/upload", publisher, upload_data), "upload", xx arena);
|
|
process.assert(get_str(up, "status") == "stored", "upload json status stored");
|
|
sha := get_str(up, "sha256");
|
|
process.assert(sha == expect_sha, "upload sha256 equals an independent digest of the bytes");
|
|
process.assert(fs.exists(path_join(STORE, concat("objects/", sha))), "object lands under objects/<sha>");
|
|
|
|
up2 := parse_body(post("/api/upload", publisher, upload_data), "re-upload", xx arena);
|
|
process.assert(get_str(up2, "sha256") == sha, "re-upload answers the same key (dedup)");
|
|
|
|
process.assert(post_code("/api/upload", publisher, "") == "411", "bodyless POST is 411");
|
|
print(" upload: content-addressed, dedup, 411 without a body\n");
|
|
|
|
// ── release publish over the uploaded object ───────────────────────
|
|
rb := parse_body(post("/api/apps/acme-app/releases", publisher, release_body("1.2.3", "beta", sha)), "release", xx arena);
|
|
process.assert(get_str(rb, "status") == "published", "release json status published");
|
|
process.assert(get_str(get_obj(rb, "release"), "id") == "rel-acme-app-1.2.3", "release id");
|
|
process.assert(channel_pointer("beta") == "rel-acme-app-1.2.3", "beta points at the new release");
|
|
|
|
det := parse_body(fetch("/api/apps/acme-app"), "detail", xx arena);
|
|
process.assert(get_arr(det, "releases").len == 1, "GET reflects the POSTed release (per-request reload)");
|
|
|
|
process.assert(post_code("/api/apps/acme-app/releases", wrong_app, release_body("1.2.9", "beta", sha)) == "403",
|
|
"token scoped to another app is 403");
|
|
process.assert(err_code(post("/api/apps/acme-app/releases", wrong_app, release_body("1.2.9", "beta", sha)), "wrong-app body", xx arena) == "auth.app_forbidden",
|
|
"wrong-app scope names auth.app_forbidden");
|
|
|
|
UNKNOWN :: "0000000000000000000000000000000000000000000000000000000000000000";
|
|
process.assert(post_code("/api/apps/acme-app/releases", publisher, release_body("9.9.9", "nightly", UNKNOWN)) == "404",
|
|
"unknown object is 404");
|
|
process.assert(err_code(post("/api/apps/acme-app/releases", publisher, release_body("9.9.9", "nightly", UNKNOWN)), "unknown-object body", xx arena) == "api.unknown_object",
|
|
"unknown object names api.unknown_object");
|
|
process.assert(channel_pointer("nightly") == "", "aborted publish leaves no channel behind");
|
|
|
|
process.assert(post_code("/api/apps/acme-app/releases", publisher, release_body("1.2.3", "beta", sha)) == "409",
|
|
"duplicate release id is 409");
|
|
print(" release: publish ok, scope enforced, aborts leave no trace\n");
|
|
|
|
// ── channel ops mirror the CLI ─────────────────────────────────────
|
|
p2 := parse_body(post("/api/apps/acme-app/releases", publisher, release_body("1.2.4", "beta", sha)), "release B", xx arena);
|
|
process.assert(get_str(p2, "status") == "published", "second release published");
|
|
process.assert(channel_pointer("beta") == "rel-acme-app-1.2.4", "beta -> 1.2.4");
|
|
|
|
rbk := parse_body(post("/api/apps/acme-app/channels/beta/rollback", publisher, "-d ''"), "rollback", xx arena);
|
|
process.assert(get_str(rbk, "status") == "rolled_back", "rollback json status");
|
|
process.assert(channel_pointer("beta") == "rel-acme-app-1.2.3", "rollback moved beta back");
|
|
|
|
pm := parse_body(post("/api/apps/acme-app/channels/beta/promote", publisher, "-d '{\"release_id\":\"rel-acme-app-1.2.4\"}'"), "promote", xx arena);
|
|
process.assert(get_str(pm, "status") == "promoted", "promote json status");
|
|
process.assert(channel_pointer("beta") == "rel-acme-app-1.2.4", "promote moved beta forward");
|
|
|
|
process.assert(post_code("/api/apps/acme-app/channels/beta/promote", publisher, "-d '{\"release_id\":\"rel-nope\"}'") == "404",
|
|
"promoting an unknown release is 404");
|
|
print(" channel ops: promote/rollback mirror the CLI\n");
|
|
|
|
// ── last-used stamping + method gate ───────────────────────────────
|
|
process.assert(token_last_used("publisher") > 0, "publisher token got last_used_at stamped");
|
|
process.assert(token_last_used("reader") == 0, "refused token is never stamped");
|
|
|
|
mc := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code}' -X DELETE ", BASE), "/healthz"));
|
|
process.assert(mc != null and mc!.stdout == "405", "non-GET/POST method is 405");
|
|
print(" last-used stamped for authed tokens only; 405 method gate\n");
|
|
|
|
// ── teardown ───────────────────────────────────────────────────────
|
|
process.run(concat("kill ", pid));
|
|
process.run(concat("rm -rf ", STORE));
|
|
print("server_write: ALL CASES PASS\n");
|
|
return 0;
|
|
}
|