88 lines
4.6 KiB
TypeScript
88 lines
4.6 KiB
TypeScript
|
|
import { test, before, after } from "node:test";
|
||
|
|
import assert from "node:assert/strict";
|
||
|
|
import PocketBase from "pocketbase";
|
||
|
|
import { authenticateUser, sessionFromToken, changeUserPassword, addUser, listUsers, setUserActive, deleteUser } from "./auth-service";
|
||
|
|
|
||
|
|
const integration = process.env.MEGA_AUTH_INTEGRATION === "1";
|
||
|
|
const admin = new PocketBase(process.env.NEXT_PUBLIC_PB_URL || "http://127.0.0.1:8090");
|
||
|
|
const email = `mega-login-qa-${Date.now()}@example.invalid`;
|
||
|
|
let userId = "";
|
||
|
|
let extraId = "";
|
||
|
|
|
||
|
|
before(async () => {
|
||
|
|
if (!integration) return;
|
||
|
|
await admin.collection("_superusers").authWithPassword(process.env.PB_ADMIN!, process.env.PB_PASS!);
|
||
|
|
userId = (await admin.collection("users").create({ name: "Temporary login test", email, password: "mega2026", passwordConfirm: "mega2026" })).id;
|
||
|
|
});
|
||
|
|
|
||
|
|
after(async () => {
|
||
|
|
if (!integration) return;
|
||
|
|
for (const id of [extraId, userId].filter(Boolean)) await admin.collection("users").delete(id);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("temporary PocketBase user must replace starter password before returning login grants access", { skip: !integration }, async () => {
|
||
|
|
await assert.rejects(authenticateUser(email, "wrong-password"));
|
||
|
|
assert.equal(await sessionFromToken("forged-token"), null);
|
||
|
|
const first = await authenticateUser(email, "mega2026");
|
||
|
|
assert.equal(first.user.id, userId);
|
||
|
|
assert.equal(first.passwordChanged, false);
|
||
|
|
const oldToken = first.pb.authStore.token;
|
||
|
|
const tampered = oldToken.split(".");
|
||
|
|
tampered[2] = "invalidsignature";
|
||
|
|
assert.equal(await sessionFromToken(tampered.join(".")), null);
|
||
|
|
const changed = await changeUserPassword(first, "abcdefgh");
|
||
|
|
assert.equal(changed.passwordChanged, true);
|
||
|
|
assert.equal(await sessionFromToken(oldToken), null);
|
||
|
|
await assert.rejects(authenticateUser(email, "mega2026"));
|
||
|
|
const returning = await authenticateUser(email, "abcdefgh");
|
||
|
|
assert.equal(returning.passwordChanged, true);
|
||
|
|
assert.equal((await sessionFromToken(returning.pb.authStore.token))?.user.id, userId);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("add user assigns starter password and duplicate email cannot replace existing user", { skip: !integration }, async () => {
|
||
|
|
const extraEmail = `new-${email}`;
|
||
|
|
const added = await addUser("New test person", extraEmail);
|
||
|
|
extraId = added.id;
|
||
|
|
assert.equal(added.name, "New test person");
|
||
|
|
assert.equal((await authenticateUser(extraEmail, "mega2026")).passwordChanged, false);
|
||
|
|
await assert.rejects(addUser("Duplicate", extraEmail));
|
||
|
|
assert.equal((await admin.collection("users").getOne(extraId)).name, "New test person");
|
||
|
|
const token = (await authenticateUser(extraEmail, "mega2026")).pb.authStore.token;
|
||
|
|
await admin.collection("users").delete(extraId);
|
||
|
|
extraId = "";
|
||
|
|
assert.equal(await sessionFromToken(token), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("inactive users lose app access immediately and reactivation preserves their password", { skip: !integration }, async () => {
|
||
|
|
const target = await addUser("Access test", `access-${email}`);
|
||
|
|
extraId = target.id;
|
||
|
|
const first = await authenticateUser(target.email, "mega2026");
|
||
|
|
await setUserActive(userId, target.id, false);
|
||
|
|
assert.equal(await sessionFromToken(first.pb.authStore.token), null);
|
||
|
|
await assert.rejects(authenticateUser(target.email, "mega2026"), /inactive/i);
|
||
|
|
await setUserActive(userId, target.id, true);
|
||
|
|
assert.equal((await authenticateUser(target.email, "mega2026")).passwordChanged, false);
|
||
|
|
const signedIn = await changeUserPassword(first, "simplepass");
|
||
|
|
const token = signedIn.pb.authStore.token;
|
||
|
|
await setUserActive(userId, target.id, false);
|
||
|
|
assert.equal(await sessionFromToken(token), null);
|
||
|
|
await assert.rejects(authenticateUser(target.email, "simplepass"), /inactive/i);
|
||
|
|
const inactive = (await listUsers()).find((user) => user.id === target.id);
|
||
|
|
assert.equal(inactive?.active, false);
|
||
|
|
assert.equal(inactive?.passwordChanged, true);
|
||
|
|
await setUserActive(userId, target.id, true);
|
||
|
|
assert.equal((await authenticateUser(target.email, "simplepass")).passwordChanged, true);
|
||
|
|
assert.equal((await listUsers()).find((user) => user.id === target.id)?.active, true);
|
||
|
|
await deleteUser(userId, target.id);
|
||
|
|
extraId = "";
|
||
|
|
assert.equal(await sessionFromToken(token), null);
|
||
|
|
assert.equal((await listUsers()).some((user) => user.id === target.id), false);
|
||
|
|
assert.equal((await admin.collection("user_onboarding").getList(1, 1, { filter: admin.filter("user = {:id}", { id: target.id }) })).totalItems, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("users cannot deactivate or delete themselves", { skip: !integration }, async () => {
|
||
|
|
await assert.rejects(setUserActive(userId, userId, false), /own account/i);
|
||
|
|
await assert.rejects(deleteUser(userId, userId), /own account/i);
|
||
|
|
assert.ok(await sessionFromToken((await authenticateUser(email, "abcdefgh")).pb.authStore.token));
|
||
|
|
});
|