aegida-console / lib / db / users.test.ts
users.test.ts
Raw
// @vitest-environment node

import { describe, expect, it } from "vitest";

import {
  findUserByEmail,
  updateLastModelId,
  upsertExternalUser,
} from "@/lib/db/users";

const validProfile = {
  id: "42",
  login: "alice",
  fullName: "Alice Smith",
  position: "Engineer",
  email: "alice@example.com",
  lastName: "Smith",
  firstName: "Alice",
  middleName: "Jane",
  roles: ["admin", "employee"],
  createdAt: "2026-07-01T12:00:00.000Z",
  updatedAt: "2026-07-02T12:00:00.000Z",
};

class RecordingDatabase {
  queries: Array<{ text: string; values?: readonly unknown[] }> = [];
  resultRows: unknown[] = [];

  async query<T>(text: string, values?: readonly unknown[]) {
    this.queries.push({ text, values });
    return { rows: this.resultRows as T[] };
  }
}

describe("user repository", () => {
  it("normalizes emails and persists the last selected model", async () => {
    const database = new RecordingDatabase();

    await findUserByEmail(database, "  DEMO@AI-CONTROL.LOCAL ");
    await updateLastModelId(database, "user-id", "qwen");

    expect(database.queries[0]).toMatchObject({
      values: ["demo@ai-control.local"],
    });
    expect(database.queries[1]).toMatchObject({
      values: ["qwen", "user-id"],
    });
  });

  it("upserts by external ID while preserving model preference", async () => {
    const database = new RecordingDatabase();
    database.resultRows = [
      {
        id: "00000000-0000-4000-8000-000000000042",
        external_user_id: "42",
        email: "alice@example.com",
        login: "alice",
        full_name: "Alice Smith",
        position: "Engineer",
        first_name: "Alice",
        last_name: "Smith",
        middle_name: "Jane",
        roles: ["admin", "employee"],
        password_hash: null,
        last_model_id: "qwen",
        created_at: "2026-07-01T12:00:00.000Z",
        updated_at: "2026-07-02T12:00:00.000Z",
      },
    ];

    await expect(upsertExternalUser(database, validProfile)).resolves.toMatchObject({
      id: "00000000-0000-4000-8000-000000000042",
      externalUserId: "42",
      roles: ["admin", "employee"],
      lastModelId: "qwen",
    });

    expect(database.queries[0]).toMatchObject({
      values: [
        "42",
        "alice@example.com",
        "alice",
        "Alice Smith",
        "Engineer",
        "Alice",
        "Smith",
        "Jane",
        JSON.stringify(["admin", "employee"]),
      ],
    });
    expect(database.queries[0]?.text).toContain("ON CONFLICT (external_user_id)");
    expect(database.queries[0]?.text).not.toContain("last_model_id = EXCLUDED");
  });

  it("atomically adopts a same-email legacy user without replacing its UUID or model", async () => {
    const database = new RecordingDatabase();
    database.resultRows = [
      {
        id: "00000000-0000-4000-8000-000000000099",
        external_user_id: "42",
        email: "alice@example.com",
        login: "alice",
        full_name: "Alice Smith",
        position: "Engineer",
        first_name: "Alice",
        last_name: "Smith",
        middle_name: "Jane",
        roles: ["admin", "employee"],
        password_hash: "legacy-password-hash",
        last_model_id: "qwen",
        created_at: "2025-01-01T12:00:00.000Z",
        updated_at: "2026-07-02T12:00:00.000Z",
      },
    ];

    await expect(upsertExternalUser(database, validProfile)).resolves.toMatchObject({
      id: "00000000-0000-4000-8000-000000000099",
      externalUserId: "42",
      lastModelId: "qwen",
      createdAt: "2025-01-01T12:00:00.000Z",
    });

    expect(database.queries).toHaveLength(1);
    expect(database.queries[0]?.text).toContain("WITH adopted AS");
    expect(database.queries[0]?.text).toContain("external_user_id IS NULL");
    expect(database.queries[0]?.text).toContain("WHERE NOT EXISTS (SELECT 1 FROM adopted)");
  });
});