0
2
import { describe, expect, test } from "vitest";
/**
* A client metadata document's `scope` bounds what that client may ever ask
* for. Conditional scopes widen a single authorization request past the base
* set, and a PDS that checks the request against the document rejects the
* whole thing when the widened request names a scope the document does not.
* blacksky.app answers PAR with `400 scope "repo:social.grain.group.submission"
* is not registered in the client metadata`; bsky.social does not check, so the
* omission is invisible on the PDS most accounts are on.
*
* The cost of the mistake is not the feature — it is every login on such a PDS.
*/
/** The prod clients only exist when APP_DOMAIN is set, so set it before load. */
process.env.APP_DOMAIN = "grain.test";
const config = (await import("../hatk.config.ts")).default;
const clients = config.oauth?.clients ?? [];
const conditional = config.oauth?.conditionalScopes ?? [];
/** Scope strings are space-separated; each one is checked whole. */
const split = (scope: string) => scope.split(" ").filter(Boolean);
test("every client is registered, prod included", () => {
expect(clients.map((c) => c.client_id)).toContain(
"https://grain.test/oauth-client-metadata.json",
);
expect(clients.length).toBeGreaterThan(1);
});
test("the group scopes a conditional adds are registered somewhere", () => {
const conditionalScopes = conditional.flatMap((c) => c.scopes).flatMap(split);
expect(conditionalScopes).toContain("repo:social.grain.group.submission");
});
describe("conditional scopes are registered on every client", () => {
for (const client of clients) {
test(client.client_id, () => {
const registered = new Set(split(client.scope));
const missing = conditional
.flatMap((c) => c.scopes)
.flatMap(split)
.filter((s) => !registered.has(s));
expect(missing).toEqual([]);
});
}
});
test("the scopes a server-initiated login requests are registered too", () => {
// `oauth.scopes` is what hatk builds its own authorization request from, so
// it is bounded by the same document as anything a client asks for.
const requested = (config.oauth?.scopes ?? []).flatMap(split);
for (const client of clients) {
const registered = new Set(split(client.scope));
expect(requested.filter((s) => !registered.has(s))).toEqual([]);
}
});