168 lines
5.9 KiB
TypeScript
168 lines
5.9 KiB
TypeScript
/**
|
|
* Reusable conformance assertions every concrete `AuthProviderAdapter` MUST
|
|
* pass. Implemented as a set of pure functions so the runner-agnostic
|
|
* assertions in this package can be driven from `@suaveplan/testing/runner`
|
|
* without forcing concrete providers onto a specific runner.
|
|
*
|
|
* `runAuthProviderConformance` returns a per-category list of scenario
|
|
* results; concrete providers wrap it in `describe`/`it` from their own test
|
|
* file (`import { describe, it, expect } from "@suaveplan/testing/runner";`).
|
|
*/
|
|
|
|
import { detectAuthPathCollisions } from "../collision-detector/collision-detector.js";
|
|
import { AUTH_PROVIDER_CODES } from "../error-codes/error-codes.js";
|
|
import { PathOwnership } from "../path-ownership/path-ownership.js";
|
|
import { isAuthPathPrefix } from "../provider/provider.js";
|
|
import type { AuthProviderAdapter } from "../provider/provider.types.js";
|
|
import { listProviderRouteNames } from "../route-surface/route-surface.js";
|
|
import type {
|
|
AuthProviderConformanceCategory,
|
|
AuthProviderConformanceOptions,
|
|
ConformanceScenarioResult,
|
|
} from "./conformance.types.js";
|
|
|
|
/**
|
|
* Returns `true` when re-registering the same provider on an existing
|
|
* registry does NOT throw (the documented idempotency invariant).
|
|
*/
|
|
export function isRegisterIdempotent(
|
|
registry: PathOwnership,
|
|
provider: AuthProviderAdapter
|
|
): boolean {
|
|
try {
|
|
registry.register(provider);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns `true` when attempting to register `incoming` against a registry
|
|
* already owned by another provider throws
|
|
* `AUTH_PROVIDER.MULTIPLE_PROVIDERS_REGISTERED`. Any other thrown shape — or
|
|
* no throw at all — returns `false`.
|
|
*/
|
|
export function rejectsSecondProvider(
|
|
registry: PathOwnership,
|
|
incoming: AuthProviderAdapter
|
|
): boolean {
|
|
try {
|
|
registry.register(incoming);
|
|
return false;
|
|
} catch (err) {
|
|
const code =
|
|
err instanceof Error && "code" in err
|
|
? (err as { readonly code: unknown }).code
|
|
: undefined;
|
|
return code === AUTH_PROVIDER_CODES.MULTIPLE_PROVIDERS_REGISTERED;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drive the harness against the supplied factory. Returns one
|
|
* `ConformanceScenarioResult` per scenario, grouped per category. Concrete
|
|
* providers iterate the result list and turn each entry into an `it()`
|
|
* assertion.
|
|
*/
|
|
export function runAuthProviderConformance(
|
|
options: AuthProviderConformanceOptions
|
|
): ReadonlyArray<ConformanceScenarioResult> {
|
|
const results: ConformanceScenarioResult[] = [];
|
|
|
|
// --- path-ownership ----------------------------------------------------
|
|
const pathOwnership: AuthProviderConformanceCategory = "path-ownership";
|
|
{
|
|
const provider = options.factory();
|
|
const registry = new PathOwnership();
|
|
registry.register(provider);
|
|
results.push({
|
|
category: pathOwnership,
|
|
name: "registers provider on first call",
|
|
passed: registry.hasProvider(),
|
|
});
|
|
results.push({
|
|
category: pathOwnership,
|
|
name: "re-registering same provider is idempotent",
|
|
passed: isRegisterIdempotent(registry, provider),
|
|
});
|
|
const second = options.factory();
|
|
const distinct: typeof second = { ...second, id: `${second.id}.alt` };
|
|
results.push({
|
|
category: pathOwnership,
|
|
name: "second provider rejected with MULTIPLE_PROVIDERS_REGISTERED",
|
|
passed: rejectsSecondProvider(registry, distinct),
|
|
});
|
|
}
|
|
|
|
// --- route-surface -----------------------------------------------------
|
|
const routeSurface: AuthProviderConformanceCategory = "route-surface";
|
|
{
|
|
const provider = options.factory();
|
|
results.push({
|
|
category: routeSurface,
|
|
name: "pathPrefix is a legal prefix",
|
|
passed: isAuthPathPrefix(provider.pathPrefix),
|
|
});
|
|
const names = listProviderRouteNames();
|
|
results.push({
|
|
category: routeSurface,
|
|
name: "canonical surface enumerates every documented name",
|
|
passed: names.length >= 9,
|
|
detail: `canonical names: ${names.join(", ")}`,
|
|
});
|
|
}
|
|
|
|
// --- lifecycle ---------------------------------------------------------
|
|
const lifecycle: AuthProviderConformanceCategory = "lifecycle";
|
|
{
|
|
const provider = options.factory();
|
|
const lc = provider.lifecycle;
|
|
results.push({
|
|
category: lifecycle,
|
|
name: "lifecycle quartet present",
|
|
passed:
|
|
typeof lc.install === "function" &&
|
|
typeof lc.start === "function" &&
|
|
typeof lc.stop === "function" &&
|
|
typeof lc.uninstall === "function",
|
|
});
|
|
}
|
|
|
|
// --- collision ---------------------------------------------------------
|
|
const collision: AuthProviderConformanceCategory = "collision";
|
|
{
|
|
const provider = options.factory();
|
|
const innerReport = detectAuthPathCollisions({
|
|
provider,
|
|
plugins: [
|
|
{
|
|
name: "plugin-x",
|
|
routes: [{ path: `${provider.pathPrefix}/custom` }],
|
|
},
|
|
],
|
|
});
|
|
results.push({
|
|
category: collision,
|
|
name: "plugin route inside auth prefix is reported",
|
|
passed: innerReport.hasCollisions,
|
|
});
|
|
const outerReport = detectAuthPathCollisions({
|
|
provider,
|
|
plugins: [
|
|
{
|
|
name: "plugin-y",
|
|
routes: [{ path: "/billing/invoices" }],
|
|
},
|
|
],
|
|
});
|
|
results.push({
|
|
category: collision,
|
|
name: "plugin route outside auth prefix is not reported",
|
|
passed: !outerReport.hasCollisions,
|
|
});
|
|
}
|
|
|
|
return Object.freeze(results);
|
|
}
|