ci: add pull request quality gates
CI / install-and-build (pull_request) Successful in 1m29s
CI / format (pull_request) Successful in 51s
CI / typecheck-source (pull_request) Successful in 41s
CI / typecheck-tests (pull_request) Successful in 39s
CI / test (pull_request) Successful in 51s
CI / lint (pull_request) Successful in 21s

Co-Authored-By: gpt-5.6-terra <noreply@openai.com>
This commit was merged in pull request #12.
This commit is contained in:
2026-07-22 21:50:29 +00:00
co-authored by Codex
parent b9fa79ff1f
commit 31d4636dde
80 changed files with 2007 additions and 398 deletions
+110 -29
View File
@@ -29,7 +29,7 @@ const MOCK_APIKEY: ClientApikey = {
};
function makeMockDb(
overrides: Partial<BackendDbInterface> = {}
overrides: Partial<BackendDbInterface> = {},
): BackendDbInterface {
return {
readonly: false,
@@ -37,42 +37,97 @@ function makeMockDb(
name: "mock",
close: async () => {},
listUsers: async () => [],
getUserById: async () => { throw new NotFoundAbodeError(); },
getUserById: async () => {
throw new NotFoundAbodeError();
},
deleteUserById: async () => {},
createUser: async () => MOCK_USER,
updateUser: async () => MOCK_USER,
getUserByEmail: async () => { throw new NotFoundAbodeError(); },
getUserByEmail: async () => {
throw new NotFoundAbodeError();
},
listAbodes: async () => [],
getAbodeById: async () => { throw new NotFoundAbodeError(); },
getAbodeById: async () => {
throw new NotFoundAbodeError();
},
deleteAbodeById: async () => {},
createAbode: async () => ({ aid: "a", name: "A", created_at: "", created_by: null, updated_at: "", updated_by: null }),
updateAbode: async () => ({ aid: "a", name: "A", created_at: "", created_by: null, updated_at: "", updated_by: null }),
createAbode: async () => ({
aid: "a",
name: "A",
created_at: "",
created_by: null,
updated_at: "",
updated_by: null,
}),
updateAbode: async () => ({
aid: "a",
name: "A",
created_at: "",
created_by: null,
updated_at: "",
updated_by: null,
}),
listResidents: async () => [],
getResidentById: async () => { throw new NotFoundAbodeError(); },
getResidentById: async () => {
throw new NotFoundAbodeError();
},
deleteResidentById: async () => {},
createResident: async () => ({ uid: "", aid: "", flags: {}, created_at: "", created_by: null, updated_at: "", updated_by: null }),
updateResident: async () => ({ uid: "", aid: "", flags: {}, created_at: "", created_by: null, updated_at: "", updated_by: null }),
createResident: async () => ({
uid: "",
aid: "",
flags: {},
created_at: "",
created_by: null,
updated_at: "",
updated_by: null,
}),
updateResident: async () => ({
uid: "",
aid: "",
flags: {},
created_at: "",
created_by: null,
updated_at: "",
updated_by: null,
}),
listResidentsByUserId: async () => [],
listResidentsByAbodeId: async () => [],
listUsersByAbodeId: async () => [],
listAbodesByUserId: async () => [],
listNotes: async () => [],
getNoteById: async () => { throw new NotFoundAbodeError(); },
getNoteById: async () => {
throw new NotFoundAbodeError();
},
deleteNoteById: async () => {},
createNote: async () => { throw new Error("unimplemented"); },
updateNote: async () => { throw new Error("unimplemented"); },
createNote: async () => {
throw new Error("unimplemented");
},
updateNote: async () => {
throw new Error("unimplemented");
},
listNotesByAbodeId: async () => [],
listNotesByUserId: async () => [],
deleteSessionsByUser: async () => {},
listApikeysByUser: async () => [],
getApikeyById: async () => { throw new NotFoundAbodeError(); },
createApikey: async () => [MOCK_APIKEY, "at_" + "0".repeat(32) as `at_${string}`],
getApikeyById: async () => {
throw new NotFoundAbodeError();
},
createApikey: async () => [
MOCK_APIKEY,
("at_" + "0".repeat(32)) as `at_${string}`,
],
deleteApikeyById: async () => {},
getUserByLogin: async () => { throw new NotFoundAbodeError(); },
getUserBySession: async () => { throw new NotFoundAbodeError(); },
getUserByLogin: async () => {
throw new NotFoundAbodeError();
},
getUserBySession: async () => {
throw new NotFoundAbodeError();
},
createSession: async () => `as_${"0".repeat(32)}`,
deleteSession: async () => {},
getUserByApikey: async () => { throw new NotFoundAbodeError(); },
getUserByApikey: async () => {
throw new NotFoundAbodeError();
},
...overrides,
};
}
@@ -92,7 +147,10 @@ type MockCtx = {
};
};
function makeMockCtx(headerOverrides: Record<string, string> = {}, cookieOverrides: Record<string, string> = {}): MockCtx {
function makeMockCtx(
headerOverrides: Record<string, string> = {},
cookieOverrides: Record<string, string> = {},
): MockCtx {
const clearedCookies = new Set<string>();
const ctx: MockCtx = {
headers: headerOverrides,
@@ -110,7 +168,10 @@ function makeMockCtx(headerOverrides: Record<string, string> = {}, cookieOverrid
return cookieOverrides[name];
},
set(name: string, value: string, opts?: unknown) {
if (value === "" || (opts && (opts as { expires?: Date }).expires?.getFullYear()! < 2000)) {
if (
value === "" ||
(opts && (opts as { expires?: Date }).expires?.getFullYear()! < 2000)
) {
clearedCookies.add(name);
}
},
@@ -121,11 +182,13 @@ function makeMockCtx(headerOverrides: Record<string, string> = {}, cookieOverrid
async function runMiddleware(
db: BackendDbInterface,
ctx: MockCtx
ctx: MockCtx,
): Promise<boolean> {
let nextCalled = false;
const mw = authenticate(db);
await mw(ctx as any, async () => { nextCalled = true; });
await mw(ctx as any, async () => {
nextCalled = true;
});
return nextCalled;
}
@@ -157,7 +220,9 @@ describe("authenticate middleware", () => {
it("wrong password (NotAuthorizedAbodeError) → 401 invalid_password", async () => {
const db = makeMockDb({
getUserByLogin: async () => { throw new NotAuthorizedAbodeError(); },
getUserByLogin: async () => {
throw new NotAuthorizedAbodeError();
},
});
const ctx = makeMockCtx({ Authorization: "Basic " + btoa("user:wrong") });
await runMiddleware(db, ctx);
@@ -167,9 +232,13 @@ describe("authenticate middleware", () => {
it("unknown user (NotFoundAbodeError) → 401 unknown_user", async () => {
const db = makeMockDb({
getUserByLogin: async () => { throw new NotFoundAbodeError(); },
getUserByLogin: async () => {
throw new NotFoundAbodeError();
},
});
const ctx = makeMockCtx({
Authorization: "Basic " + btoa("nobody:pass"),
});
const ctx = makeMockCtx({ Authorization: "Basic " + btoa("nobody:pass") });
await runMiddleware(db, ctx);
assert.equal(ctx.status, 401);
assert.deepEqual((ctx.body as any)?.error, "unknown_user");
@@ -177,7 +246,9 @@ describe("authenticate middleware", () => {
it("ConflictAbodeError (#unset password) → 401 user_not_loggable", async () => {
const db = makeMockDb({
getUserByLogin: async () => { throw new ConflictAbodeError(); },
getUserByLogin: async () => {
throw new ConflictAbodeError();
},
});
const ctx = makeMockCtx({ Authorization: "Basic " + btoa("user:pass") });
await runMiddleware(db, ctx);
@@ -211,7 +282,9 @@ describe("authenticate middleware", () => {
it("invalid/expired at_ token → 401 invalid_apikey", async () => {
const db = makeMockDb({
getUserByApikey: async () => { throw new NotFoundAbodeError(); },
getUserByApikey: async () => {
throw new NotFoundAbodeError();
},
});
const ctx = makeMockCtx({ Authorization: `Bearer ${validToken}` });
await runMiddleware(db, ctx);
@@ -246,17 +319,25 @@ describe("authenticate middleware", () => {
const db = makeMockDb();
const ctx = makeMockCtx({}, { abode_session: "not-a-session-token" });
await runMiddleware(db, ctx);
assert.ok(ctx.clearedCookies.has("abode_session"), "cookie should be cleared");
assert.ok(
ctx.clearedCookies.has("abode_session"),
"cookie should be cleared",
);
assert.equal(ctx.status, 401);
});
it("expired/unknown session token clears cookie and returns 401", async () => {
const db = makeMockDb({
getUserBySession: async () => { throw new NotFoundAbodeError(); },
getUserBySession: async () => {
throw new NotFoundAbodeError();
},
});
const ctx = makeMockCtx({}, { abode_session: validToken });
await runMiddleware(db, ctx);
assert.ok(ctx.clearedCookies.has("abode_session"), "cookie should be cleared");
assert.ok(
ctx.clearedCookies.has("abode_session"),
"cookie should be cleared",
);
assert.equal(ctx.status, 401);
});
});