Files
chat/api/server/services/iamUser.spec.js
zooqueen e937ba8c71 a new account is met by a card naming what is here
Four lines on the empty chat: the model picker, the modes and their sources,
/agent, and the sidebar. It dims nothing and gates nothing -- the arrival modal
was removed by owner call, and this sits beside the product rather than in
front of it.

`toured` carries no schema default. `false` shows the card, `true` has seen it,
and ABSENT is an account older than the card, which never gets one; a default
reaches every record that lacks the field, which is every account that already
exists. `reconcileUser` writes `false` at creation, so exactly the accounts born
from here on qualify. The client tests `=== false` for the same reason, and the
spec fails when that is weakened to a falsy check.

It rides out on `GET /v1/chat/user`, which already sends the projected document,
so there is no second GET to keep in step -- one POST records the dismissal, and
the cached user is written on mutate so the card goes on the click rather than a
round-trip later.

In the flow, under the composer, not floating over it. The composer block is
min-h-[280px] at the bottom of that column and its input is capped and centred,
leaving ~142px of gutter at 1440 against a card that needs 304 -- measured in
Chromium against the built stylesheet: 288x252 at 16px from the column edge,
clear of the composer at 1440 and at 390, no sideways scroll, 17.25:1 on the
title and 12.82:1 on the lines.
2026-08-17 16:38:35 -07:00

108 lines
4.1 KiB
JavaScript

jest.mock('@hanzochat/data-schemas', () => ({
logger: { error: jest.fn(), warn: jest.fn(), debug: jest.fn(), info: jest.fn() },
}));
jest.mock('@hanzochat/data-provider', () => ({ SystemRoles: { USER: 'USER', ADMIN: 'ADMIN' } }));
const mockFindOpenIDUser = jest.fn();
jest.mock('@hanzochat/api', () => ({
findOpenIDUser: (...args) => mockFindOpenIDUser(...args),
getBalanceConfig: () => ({ enabled: false }),
}));
jest.mock('~/server/services/Config', () => ({ getAppConfig: async () => ({}) }));
const mockFindUser = jest.fn();
const mockCreateUser = jest.fn();
const mockUpdateUser = jest.fn();
jest.mock('~/models', () => ({
findUser: (...args) => mockFindUser(...args),
createUser: (...args) => mockCreateUser(...args),
updateUser: (...args) => mockUpdateUser(...args),
countUsers: async () => 7,
}));
const { reconcileUser } = require('./iamUser');
const claims = { sub: 'iam-subject-1', email: '[email protected]', name: 'Newcomer', owner: 'hanzo' };
describe('the local record behind an IAM identity', () => {
beforeEach(() => {
jest.clearAllMocks();
mockFindOpenIDUser.mockResolvedValue({ user: null, error: null, migration: false });
});
/**
* A first sign-in arrives as a burst — the app opens with a dozen
* authenticated requests at once, all carrying the same token, none of which
* has a record to read yet. The record has to be created once and read by all
* of them, because a request that cannot resolve the identity answers 401 to
* a visitor who has just signed in.
*/
it('creates the record once for a burst of first requests', async () => {
let created = null;
mockCreateUser.mockImplementation(async (fields) => {
if (created) {
throw new Error('E11000 duplicate key');
}
created = { _id: 'user-1', ...fields };
return created;
});
const burst = await Promise.all([...Array(12)].map(() => reconcileUser(claims, 'token')));
expect(mockCreateUser).toHaveBeenCalledTimes(1);
for (const user of burst) {
expect(user).toBe(created);
}
});
/**
* The welcome card is for accounts that start here, so the flag is written at
* creation rather than defaulted on the schema — a default reaches every
* record that lacks the field, which is every account that already exists.
*/
it('marks a new record as never having seen the welcome card', async () => {
mockCreateUser.mockImplementation(async (fields) => ({ _id: 'user-1', ...fields }));
await reconcileUser(claims, 'token');
expect(mockCreateUser).toHaveBeenCalledWith(
expect.objectContaining({ toured: false }),
expect.anything(),
true,
true,
);
});
/**
* An identity has exactly one record and the unique index decides who writes
* it, so a caller that loses the insert reads what won rather than failing.
*/
it('reads the record when another writer created it first', async () => {
const winner = { _id: 'user-1', openidId: claims.sub };
mockCreateUser.mockRejectedValue(new Error('E11000 duplicate key'));
mockFindUser.mockResolvedValue(winner);
await expect(reconcileUser(claims, 'token')).resolves.toBe(winner);
expect(mockFindUser).toHaveBeenCalledWith({ openidId: claims.sub });
});
/** A create that failed for any other reason is still a failure. */
it('refuses when the record cannot be written or found', async () => {
mockCreateUser.mockRejectedValue(new Error('disk full'));
mockFindUser.mockResolvedValue(null);
await expect(reconcileUser(claims, 'token')).rejects.toThrow('disk full');
});
/** The in-flight reconcile is dropped when it settles, never kept as a cache. */
it('resolves the identity again after the burst has settled', async () => {
const existing = { _id: 'user-1', openidId: claims.sub, provider: 'openid', role: 'USER' };
mockCreateUser.mockResolvedValue(existing);
await reconcileUser(claims, 'token');
mockFindOpenIDUser.mockResolvedValue({ user: existing, error: null, migration: false });
await expect(reconcileUser(claims, 'token')).resolves.toBe(existing);
expect(mockCreateUser).toHaveBeenCalledTimes(1);
});
});