mega-plant/lib/auth-service.test.ts
Dave 2594dead84 feat: add user access and improve plant dashboard
Add PocketBase login, first-sign-in password changes, account settings,
and user activation, reactivation and deletion with self-removal guards.
Protect application pages and APIs and keep private pages out of PWA
caches.

Resolve plant locations from site assignments and improve map controls,
profit chart views, navigation and mobile table layouts.

Validation: production build and TypeScript passed; 28 unit tests passed.
Backend access integration tests and mobile verification are documented.
2026-10-06 04:02:08 +02:00

87 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));
});