~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
58 lines · 2.3 KB Raw History
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([]);
  }
});