92 lines
4.3 KiB
TypeScript
92 lines
4.3 KiB
TypeScript
import { afterEach, expect, test } from "vitest";
|
|
import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { stringify } from "yaml";
|
|
import { buildApp, type AppWithAuthSessionStore } from "../src/app.js";
|
|
import { loadAuthenticationConfig } from "../src/auth/config.js";
|
|
import { loadConfig } from "../src/config.js";
|
|
import { createFixtureAuthStorageBridge, prepareAuthStateRoot } from "./auth-test-fixtures.js";
|
|
|
|
const password = "correct horse battery staple";
|
|
const passwordHash = "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4";
|
|
const userA = { id: "6ba7b810-9dad-4ed1-80b4-00c04fd430c8", username: "AdminA" };
|
|
const userB = { id: "6ba7b811-9dad-4ed1-80b4-00c04fd430c8", username: "AdminB" };
|
|
const publicUrl = "http://127.0.0.1:8787";
|
|
const cleanups: Array<() => Promise<void>> = [];
|
|
|
|
afterEach(async () => {
|
|
for (const cleanup of cleanups.splice(0).reverse()) await cleanup();
|
|
});
|
|
|
|
function usersYaml(user: typeof userA): string {
|
|
return [
|
|
"version: 1", "users:", ` - id: ${user.id}`, ` username: ${user.username}`,
|
|
` passwordHash: ${passwordHash}`, " roles:", " - admin", " enabled: true", " authRevision: 1", "",
|
|
].join("\n");
|
|
}
|
|
|
|
function configYaml(usersFile: string) {
|
|
return stringify({ version: 1, mode: "local", publicUrl, local: { usersFile } });
|
|
}
|
|
|
|
function cookiePair(response: { headers: Record<string, string | string[] | undefined> }): string {
|
|
const header = response.headers["set-cookie"];
|
|
const first = Array.isArray(header) ? header[0] : header;
|
|
return first?.split(";", 1)[0] ?? "";
|
|
}
|
|
|
|
test("each login and session resolve uses the current config snapshot users file", async () => {
|
|
const directory = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-dynamic-"));
|
|
chmodSync(directory, 0o700);
|
|
const authFile = join(directory, "auth.yaml");
|
|
const usersAFile = join(directory, "users-a.yaml");
|
|
const usersBFile = join(directory, "users-bbbbb.yaml");
|
|
writeFileSync(usersAFile, usersYaml(userA), { encoding: "utf8", mode: 0o600 });
|
|
writeFileSync(usersBFile, usersYaml(userB), { encoding: "utf8", mode: 0o600 });
|
|
writeFileSync(authFile, configYaml("users-a.yaml"), { encoding: "utf8", mode: 0o600 });
|
|
chmodSync(authFile, 0o600);
|
|
chmodSync(usersAFile, 0o600);
|
|
chmodSync(usersBFile, 0o600);
|
|
const authStateRoot = join(directory, "auth-state");
|
|
prepareAuthStateRoot(authStateRoot);
|
|
const app = buildApp(loadConfig({
|
|
THT_AUTH_CONFIG_FILE: authFile,
|
|
THT_AUTH_STATE_ROOT: authStateRoot,
|
|
THT_HARNESS_DIR: "/tmp/h",
|
|
}), { authStorageBridgeForTest: createFixtureAuthStorageBridge() });
|
|
cleanups.push(async () => {
|
|
await app.close();
|
|
rmSync(directory, { recursive: true, force: true });
|
|
});
|
|
const signIn = (username: string) => app.inject({
|
|
method: "POST", url: "/auth/local/login",
|
|
headers: { origin: publicUrl, "sec-fetch-site": "same-origin" },
|
|
payload: { username, password },
|
|
});
|
|
|
|
const first = await signIn(userA.username);
|
|
expect(first.statusCode).toBe(200);
|
|
const aCookie = cookiePair(first);
|
|
expect((await app.inject({ method: "GET", url: "/me", headers: { cookie: aCookie } })).json())
|
|
.toMatchObject({ subject: userA.id });
|
|
|
|
writeFileSync(authFile, configYaml("users-bbbbb.yaml"), { encoding: "utf8", mode: 0o600 });
|
|
chmodSync(authFile, 0o600);
|
|
expect(readFileSync(usersAFile, "utf8")).toContain(userA.username);
|
|
|
|
const removedUser = await signIn(userA.username);
|
|
expect(removedUser.statusCode).toBe(401);
|
|
expect(removedUser.json()).toEqual({ code: "invalid_credentials", error: "Invalid username or password" });
|
|
expect((await app.inject({ method: "GET", url: "/me", headers: { cookie: aCookie } })).statusCode).toBe(401);
|
|
|
|
const second = await signIn(userB.username);
|
|
expect(second.statusCode).toBe(200);
|
|
const bCookie = cookiePair(second);
|
|
expect(await app.inject({ method: "GET", url: "/me", headers: { cookie: bCookie } }).then((response) => response.json()))
|
|
.toMatchObject({ subject: userB.id });
|
|
const token = bCookie.split("=", 2)[1] ?? "";
|
|
const record = await (app as AppWithAuthSessionStore).thothiiAuthSessionStore?.resolve(token);
|
|
expect(record).toMatchObject({ subject: userB.id, authConfigRevision: loadAuthenticationConfig(authFile).revision });
|
|
});
|