Files
ftb-project-management/apps/server/src/modules/data/data.service.spec.ts
2026-07-02 09:30:50 +08:00

166 lines
5.7 KiB
TypeScript

import { BadRequestException, ConflictException } from '@nestjs/common';
import { DataService } from './data.service';
describe('DataService', () => {
const makeService = () => {
const prisma = {
appData: {
create: jest.fn(),
findUnique: jest.fn(),
updateMany: jest.fn(),
upsert: jest.fn(),
},
};
return {
prisma,
service: new DataService(prisma as any),
};
};
it('returns null for an allowed key with no stored value', async () => {
const { prisma, service } = makeService();
prisma.appData.findUnique.mockResolvedValue(null);
await expect(service.get('products-overview')).resolves.toEqual({
key: 'products-overview',
value: null,
version: null,
});
});
it('returns the AppData version for a stored value', async () => {
const { prisma, service } = makeService();
const updatedAt = new Date('2026-07-02T08:00:00.000Z');
prisma.appData.findUnique.mockResolvedValue({
key: 'products-overview',
value: [{ id: 'p1' }],
updatedAt,
});
await expect(service.get('products-overview')).resolves.toEqual({
key: 'products-overview',
value: [{ id: 'p1' }],
version: updatedAt.toISOString(),
});
});
it('rejects unknown keys', async () => {
const { service } = makeService();
await expect(service.get('unknown-key')).rejects.toBeInstanceOf(BadRequestException);
});
it('upserts JSON values for allowed keys', async () => {
const { prisma, service } = makeService();
const value = [{ id: 'p1', name: 'Product 1' }];
const updatedAt = new Date('2026-07-02T08:01:00.000Z');
prisma.appData.upsert.mockResolvedValue({ key: 'products-overview', value, updatedAt });
await expect(service.put('products-overview', value)).resolves.toEqual({
key: 'products-overview',
value,
version: updatedAt.toISOString(),
});
expect(prisma.appData.upsert).toHaveBeenCalledWith({
where: { key: 'products-overview' },
update: { value },
create: { key: 'products-overview', value },
});
});
it('allows supporting business data keys migrated from browser storage', async () => {
const { prisma, service } = makeService();
const value: unknown[] = [];
prisma.appData.upsert.mockImplementation(({ where }) =>
Promise.resolve({ key: where.key, value, updatedAt: new Date('2026-07-02T08:02:00.000Z') }),
);
await expect(service.put('task-worklogs', value)).resolves.toMatchObject({ key: 'task-worklogs', value });
await expect(service.put('work-activities', value)).resolves.toMatchObject({ key: 'work-activities', value });
await expect(service.put('overtime', { records: [], reasons: [] })).resolves.toMatchObject({
key: 'overtime',
value,
});
});
it('updates only when the supplied version matches the stored row version', async () => {
const { prisma, service } = makeService();
const previousVersion = '2026-07-02T08:03:00.000Z';
const nextUpdatedAt = new Date('2026-07-02T08:04:00.000Z');
const nextValue = [{ id: 'p2', name: 'Product 2' }];
prisma.appData.updateMany.mockResolvedValue({ count: 1 });
prisma.appData.findUnique.mockResolvedValue({
key: 'products-overview',
value: nextValue,
updatedAt: nextUpdatedAt,
});
await expect(service.put('products-overview', nextValue, previousVersion)).resolves.toEqual({
key: 'products-overview',
value: nextValue,
version: nextUpdatedAt.toISOString(),
});
expect(prisma.appData.updateMany).toHaveBeenCalledWith({
where: {
key: 'products-overview',
updatedAt: new Date(previousVersion),
},
data: { value: nextValue },
});
});
it('rejects stale AppData versions without overwriting the current value', async () => {
const { prisma, service } = makeService();
prisma.appData.updateMany.mockResolvedValue({ count: 0 });
prisma.appData.findUnique.mockResolvedValue({
key: 'products-overview',
value: [{ id: 'current' }],
updatedAt: new Date('2026-07-02T08:05:00.000Z'),
});
await expect(
service.put('products-overview', [{ id: 'stale' }], '2026-07-02T08:03:00.000Z'),
).rejects.toBeInstanceOf(ConflictException);
expect(prisma.appData.upsert).not.toHaveBeenCalled();
});
it('creates a missing row only when the client loaded a null version', async () => {
const { prisma, service } = makeService();
const value = [{ id: 'p1' }];
const updatedAt = new Date('2026-07-02T08:06:00.000Z');
prisma.appData.create.mockResolvedValue({ key: 'products-overview', value, updatedAt });
await expect(service.put('products-overview', value, null)).resolves.toEqual({
key: 'products-overview',
value,
version: updatedAt.toISOString(),
});
expect(prisma.appData.create).toHaveBeenCalledWith({
data: { key: 'products-overview', value },
});
expect(prisma.appData.upsert).not.toHaveBeenCalled();
});
it('rejects create-only writes when another client created the row first', async () => {
const { prisma, service } = makeService();
prisma.appData.create.mockRejectedValue({ code: 'P2002' });
prisma.appData.findUnique.mockResolvedValue({
key: 'products-overview',
value: [{ id: 'current' }],
updatedAt: new Date('2026-07-02T08:07:00.000Z'),
});
await expect(service.put('products-overview', [{ id: 'new' }], null)).rejects.toBeInstanceOf(
ConflictException,
);
});
it('rejects invalid AppData versions', async () => {
const { service } = makeService();
await expect(service.put('products-overview', [], 'not-a-date')).rejects.toBeInstanceOf(
BadRequestException,
);
});
});