Tests refactor

This commit is contained in:
Aerilyn Weber 2026-05-19 11:06:03 +09:00
parent 245520fb50
commit 99134d8556
165 changed files with 911 additions and 531 deletions

View file

@ -0,0 +1,128 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Use vi.hoisted so mocks are available in vi.mock factory (which is hoisted)
const { mockLean, mockExec, mockFindOne, mockFindById, mockFindOneAndUpdate, mockSave } =
vi.hoisted(() => {
const mockExec = vi.fn();
const mockLean = vi.fn(() => ({ exec: mockExec }));
return {
mockExec,
mockLean,
mockFindOne: vi.fn(() => ({ lean: mockLean })),
mockFindById: vi.fn(() => ({ lean: mockLean })),
mockFindOneAndUpdate: vi.fn(() => ({ exec: mockExec })),
mockSave: vi.fn(),
};
});
vi.mock('../../../src/schemas/user.schema.js', () => {
class MockUserModel {
_data: Record<string, unknown>;
constructor(data: Record<string, unknown>) {
this._data = data;
Object.assign(this, data);
}
save() {
mockSave();
return Promise.resolve(this);
}
toObject() {
return { _id: 'new-id', ...this._data };
}
static findOne = mockFindOne;
static findById = mockFindById;
static findOneAndUpdate = mockFindOneAndUpdate;
}
return { UserModel: MockUserModel };
});
import { UsersRepository } from '../../../src/modules/users/users.repository.js';
describe('UsersRepository', () => {
let repo: UsersRepository;
beforeEach(() => {
vi.clearAllMocks();
repo = new UsersRepository();
});
describe('findByKeycloakId', () => {
it('calls findOne with keycloakId and returns lean result', async () => {
const user = { _id: 'u1', keycloakId: 'kc-1' };
mockExec.mockResolvedValue(user);
const result = await repo.findByKeycloakId('kc-1');
expect(mockFindOne).toHaveBeenCalledWith({ keycloakId: 'kc-1' }, null, {
session: undefined,
});
expect(mockLean).toHaveBeenCalled();
expect(mockExec).toHaveBeenCalled();
expect(result).toEqual(user);
});
});
describe('findById', () => {
it('calls findById and returns lean result', async () => {
const user = { _id: 'u1' };
mockExec.mockResolvedValue(user);
const result = await repo.findById('u1');
expect(mockFindById).toHaveBeenCalledWith('u1');
expect(result).toEqual(user);
});
});
describe('create', () => {
it('creates a new user and returns plain object', async () => {
mockSave.mockResolvedValue({});
const data = {
keycloakId: 'kc-1',
email: 'test@example.com',
displayName: 'Test',
householdIds: [],
};
const result = await repo.create(data as never);
expect(mockSave).toHaveBeenCalled();
expect(result).toMatchObject({ keycloakId: 'kc-1' });
});
});
describe('update', () => {
it('calls findOneAndUpdate with $set', async () => {
const updated = { _id: 'u1', displayName: 'Updated' };
mockExec.mockResolvedValue(updated);
const result = await repo.update('kc-1', { displayName: 'Updated' } as never);
expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
{ keycloakId: 'kc-1' },
{ $set: { displayName: 'Updated' } },
{ new: true, lean: true, session: undefined },
);
expect(result).toEqual(updated);
});
});
describe('upsertFromToken', () => {
it('upserts user with $set and $setOnInsert', async () => {
const upserted = { _id: 'u1', keycloakId: 'kc-1' };
mockExec.mockResolvedValue(upserted);
const result = await repo.upsertFromToken('kc-1', 'a@b.com', 'Name');
expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
{ keycloakId: 'kc-1' },
{
$set: { email: 'a@b.com', displayName: 'Name' },
$setOnInsert: { keycloakId: 'kc-1', householdIds: [], defaultHouseholdId: null },
},
{ upsert: true, new: true, lean: true },
);
expect(result).toEqual(upserted);
});
});
});

View file

@ -0,0 +1,135 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import Fastify from 'fastify';
import { fastifyAwilixPlugin } from '@fastify/awilix';
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
// Mock jose for auth plugin
vi.mock('jose', () => ({
createRemoteJWKSet: vi.fn(() => 'mock-jwks'),
jwtVerify: vi.fn().mockResolvedValue({
payload: {
sub: 'kc-1',
email: 'test@example.com',
preferred_username: 'testuser',
realm_access: { roles: ['member'] },
householdIds: ['hh1'],
},
protectedHeader: { alg: 'RS256' },
key: {},
}),
}));
// Mock the users repository module with a real class
const { mockUpsertFromToken, mockFindByKeycloakId } = vi.hoisted(() => ({
mockUpsertFromToken: vi.fn(),
mockFindByKeycloakId: vi.fn(),
}));
vi.mock('../../../src/modules/users/users.repository.js', () => ({
UsersRepository: class MockUsersRepository {
upsertFromToken = mockUpsertFromToken;
findByKeycloakId = mockFindByKeycloakId;
},
}));
import authPlugin from '../../../src/plugins/auth.plugin.js';
import usersRoutes from '../../../src/modules/users/users.routes.js';
describe('users.routes', () => {
async function buildTestApp() {
const app = Fastify({ logger: false });
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
await app.register(fastifyAwilixPlugin, {
disposeOnClose: true,
disposeOnResponse: true,
strictBooleanEnforced: true,
});
await app.register(authPlugin);
await app.register(usersRoutes);
return app;
}
beforeEach(() => {
vi.clearAllMocks();
mockFindByKeycloakId.mockResolvedValue({ householdIds: ['hh1'] });
});
it('returns 404 when syncFromToken returns null', async () => {
mockUpsertFromToken.mockResolvedValue(null);
const app = await buildTestApp();
await app.ready();
const res = await app.inject({
method: 'GET',
url: '/api/v1/users/me',
headers: { authorization: 'Bearer valid-token' },
});
expect(res.statusCode).toBe(404);
await app.close();
});
it('handles ObjectId and Date objects in response', async () => {
const mockUser = {
_id: { toString: () => 'u-obj' },
keycloakId: 'kc-1',
displayName: 'testuser',
email: 'test@example.com',
householdIds: ['hh1'],
defaultHouseholdId: 'hh1',
createdAt: { toISOString: () => '2024-01-01T00:00:00.000Z' },
updatedAt: { toISOString: () => '2024-01-02T00:00:00.000Z' },
};
mockUpsertFromToken.mockResolvedValue(mockUser);
const app = await buildTestApp();
await app.ready();
const res = await app.inject({
method: 'GET',
url: '/api/v1/users/me',
headers: { authorization: 'Bearer valid-token' },
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body._id).toBe('u-obj');
expect(body.createdAt).toBe('2024-01-01T00:00:00.000Z');
expect(body.defaultHouseholdId).toBe('hh1');
await app.close();
});
it('GET /api/v1/users/me syncs user from token and returns profile', async () => {
const mockUser = {
_id: 'u1',
keycloakId: 'kc-1',
displayName: 'testuser',
email: 'test@example.com',
householdIds: ['hh1'],
defaultHouseholdId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
mockUpsertFromToken.mockResolvedValue(mockUser);
const app = await buildTestApp();
await app.ready();
const res = await app.inject({
method: 'GET',
url: '/api/v1/users/me',
headers: { authorization: 'Bearer valid-token' },
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.keycloakId).toBe('kc-1');
expect(body.email).toBe('test@example.com');
expect(mockUpsertFromToken).toHaveBeenCalledWith('kc-1', 'test@example.com', 'testuser');
await app.close();
});
});

View file

@ -0,0 +1,61 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UsersService } from '../../../src/modules/users/users.service.js';
import { NotFoundError } from '../../../src/common/errors.js';
describe('UsersService', () => {
const mockRepo = {
findByKeycloakId: vi.fn(),
findById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
upsertFromToken: vi.fn(),
};
let service: UsersService;
beforeEach(() => {
vi.clearAllMocks();
service = new UsersService({ usersRepository: mockRepo as never });
});
describe('syncFromToken', () => {
it('upserts user from auth token data', async () => {
const authUser = {
keycloakId: 'kc-1',
email: 'test@example.com',
displayName: 'Test User',
roles: ['member'],
householdIds: [],
};
const upserted = { _id: 'u1', ...authUser };
mockRepo.upsertFromToken.mockResolvedValue(upserted);
const result = await service.syncFromToken(authUser);
expect(mockRepo.upsertFromToken).toHaveBeenCalledWith(
'kc-1',
'test@example.com',
'Test User',
);
expect(result).toEqual(upserted);
});
});
describe('getProfile', () => {
it('returns user when found', async () => {
const user = { _id: 'u1', keycloakId: 'kc-1', displayName: 'Test' };
mockRepo.findByKeycloakId.mockResolvedValue(user);
const result = await service.getProfile('kc-1');
expect(result).toEqual(user);
expect(mockRepo.findByKeycloakId).toHaveBeenCalledWith('kc-1');
});
it('throws NotFoundError when user not found', async () => {
mockRepo.findByKeycloakId.mockResolvedValue(null);
await expect(service.getProfile('kc-missing')).rejects.toThrow(NotFoundError);
});
});
});