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, ); }); });