db.test.js 4.62 KB
// RED/GREEN check for the SQLite-backed users/sessions store (dev-plan-3.md
// T18). Run: node --test db.test.js
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { openDb, createUsersStore, createSessionsStore } = require("./db.js");

function tmpDbPath() {
  return path.join(fs.mkdtempSync(path.join(os.tmpdir(), "dressup-db-test-")), "data.sqlite");
}

test("users: create/get/findByEmail round-trip, defaults filled in", () => {
  const db = openDb(tmpDbPath());
  const users = createUsersStore(db);

  const user = users.create({ email: "a@example.com", passwordHash: "hash1" });
  assert.ok(user.id);
  assert.equal(user.email, "a@example.com");
  assert.equal(user.emailVerified, false);
  assert.equal(user.role, "viewer");
  assert.deepEqual(user.oauthProviders, []);
  assert.equal(user.creditBalance, 0);

  assert.deepEqual(users.get(user.id), user);
  assert.deepEqual(users.findByEmail("a@example.com"), user);
  assert.equal(users.findByEmail("nope@example.com"), null);
  assert.equal(users.get("nonexistent-id"), null);
});

test("users: duplicate email is rejected as a conflict, not a second row", () => {
  const db = openDb(tmpDbPath());
  const users = createUsersStore(db);

  users.create({ email: "dup@example.com", passwordHash: "hash1" });
  assert.throws(() => users.create({ email: "dup@example.com", passwordHash: "hash2" }), (err) => err.code === "EMAIL_TAKEN");
});

test("users: update() patches only whitelisted fields, ignores the rest", () => {
  const db = openDb(tmpDbPath());
  const users = createUsersStore(db);
  const user = users.create({ email: "b@example.com", passwordHash: "hash1" });

  const updated = users.update(user.id, { emailVerified: true, nickname: "小美", id: "should-be-ignored", email: "should-be-ignored@example.com" });
  assert.equal(updated.emailVerified, true);
  assert.equal(updated.nickname, "小美");
  assert.equal(updated.id, user.id);
  assert.equal(updated.email, "b@example.com"); // unchanged original email, patch ignored
});

test("users: legacyUid/ageConfirmed carry over from an existing anonymous cookie (T21's use case)", () => {
  const db = openDb(tmpDbPath());
  const users = createUsersStore(db);
  const ts = Date.now();

  const user = users.create({
    email: "c@example.com",
    passwordHash: "hash1",
    nickname: "老观众",
    legacyUid: "viewer-uid-123",
    ageConfirmed: true,
    ageConfirmedAt: ts,
  });
  assert.equal(user.legacyUid, "viewer-uid-123");
  assert.equal(user.ageConfirmed, true);
  assert.equal(user.ageConfirmedAt, ts);
});

test("data survives reopening the same file path (simulated process restart)", () => {
  const dbPath = tmpDbPath();
  const db1 = openDb(dbPath);
  const users1 = createUsersStore(db1);
  const created = users1.create({ email: "restart@example.com", passwordHash: "hash1" });
  db1.close();

  const db2 = openDb(dbPath);
  const users2 = createUsersStore(db2);
  assert.deepEqual(users2.get(created.id), created);
});

test("sessions: create/findByToken round-trip", () => {
  const db = openDb(tmpDbPath());
  const users = createUsersStore(db);
  const sessions = createSessionsStore(db);
  const user = users.create({ email: "d@example.com", passwordHash: "hash1" });

  const session = sessions.create({ userId: user.id, kind: "session", expiresAt: Date.now() + 100000 });
  assert.ok(session.token);
  assert.equal(sessions.findByToken(session.token).id, session.id);
  assert.equal(sessions.findByToken("nonexistent-token"), null);
});

test("sessions: an expired token is treated as absent and reaped", () => {
  const db = openDb(tmpDbPath());
  const users = createUsersStore(db);
  const sessions = createSessionsStore(db);
  const user = users.create({ email: "e@example.com", passwordHash: "hash1" });

  const session = sessions.create({ userId: user.id, kind: "email_verify", expiresAt: Date.now() - 1000 });
  assert.equal(sessions.findByToken(session.token), null);
  // reaped, not just filtered — a second lookup finds nothing to delete either
  assert.equal(sessions.findByToken(session.token), null);
});

test("sessions: revoke() logs a session out immediately", () => {
  const db = openDb(tmpDbPath());
  const users = createUsersStore(db);
  const sessions = createSessionsStore(db);
  const user = users.create({ email: "f@example.com", passwordHash: "hash1" });

  const session = sessions.create({ userId: user.id, kind: "session", expiresAt: Date.now() + 100000 });
  assert.ok(sessions.findByToken(session.token));
  sessions.revoke(session.token);
  assert.equal(sessions.findByToken(session.token), null);
});