- SiteSettings gains websiteUrl (e.g. online shop link) alongside the
existing social links, editable in Verwaltung > Automatisierung.
- /herkunft/[lot] now shows the apiary's Standort instead of its internal
Name (so multiple apiaries at one location read as one origin), adds
small icons per social link, and surfaces previously-fetched-but-unused
data (Honigsorte, Abfülldatum, stat cards) to make the page a richer
customer-facing info page.
- Backup schedule (path/cron/retention) is now also configurable via the
GUI (Verwaltung > Backup), stored in a new BackupSettings singleton;
an env var always wins over the GUI value per field, and the scheduler
reloads its cron job live when GUI settings change.
- New Automatisierung rules let an admin define a format (e.g.
"WH-{YYYY}-{SEQ:4}") that auto-generates HoneyBatch.batchNumber and
BottlingBatch.lotNumber on creation, with an atomic per-rule sequence
counter and a manual reset. Manual entry still works when the rule is
inactive.
132 lines
3.7 KiB
TypeScript
132 lines
3.7 KiB
TypeScript
import { BackupSettingsService } from './backup-settings.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { AuditLogService } from '../audit/audit-log.service';
|
|
|
|
describe('BackupSettingsService', () => {
|
|
let service: BackupSettingsService;
|
|
let prisma: {
|
|
backupSettings: {
|
|
findUnique: jest.Mock;
|
|
create: jest.Mock;
|
|
update: jest.Mock;
|
|
};
|
|
};
|
|
let auditLog: { record: jest.Mock };
|
|
|
|
const existing = {
|
|
id: 'singleton',
|
|
storagePath: '/data/backups',
|
|
cronSchedule: '0 3 * * *',
|
|
retentionCount: 14,
|
|
updatedAt: new Date('2026-01-01'),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
prisma = {
|
|
backupSettings: {
|
|
findUnique: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
};
|
|
auditLog = { record: jest.fn().mockResolvedValue(undefined) };
|
|
service = new BackupSettingsService(
|
|
prisma as unknown as PrismaService,
|
|
auditLog as unknown as AuditLogService,
|
|
);
|
|
});
|
|
|
|
describe('get', () => {
|
|
it('returns the existing singleton row', async () => {
|
|
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
|
|
|
const result = await service.get();
|
|
|
|
expect(result).toEqual(existing);
|
|
expect(prisma.backupSettings.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('creates the singleton row on first access', async () => {
|
|
prisma.backupSettings.findUnique.mockResolvedValue(null);
|
|
prisma.backupSettings.create.mockResolvedValue({
|
|
id: 'singleton',
|
|
storagePath: null,
|
|
cronSchedule: null,
|
|
retentionCount: null,
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
const result = await service.get();
|
|
|
|
expect(prisma.backupSettings.create).toHaveBeenCalledWith({
|
|
data: { id: 'singleton' },
|
|
});
|
|
expect(result.id).toBe('singleton');
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('stores a valid value and records an audit entry', async () => {
|
|
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
|
prisma.backupSettings.update.mockResolvedValue({
|
|
...existing,
|
|
retentionCount: 30,
|
|
});
|
|
|
|
const result = await service.update({ retentionCount: 30 }, 'user-1');
|
|
|
|
expect(prisma.backupSettings.update).toHaveBeenCalledWith({
|
|
where: { id: 'singleton' },
|
|
data: {
|
|
storagePath: undefined,
|
|
cronSchedule: undefined,
|
|
retentionCount: 30,
|
|
},
|
|
});
|
|
expect(result.retentionCount).toBe(30);
|
|
expect(auditLog.record).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
entityType: 'BackupSettings',
|
|
action: 'UPDATE',
|
|
userId: 'user-1',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('clears a string field when given an empty string', async () => {
|
|
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
|
prisma.backupSettings.update.mockResolvedValue({
|
|
...existing,
|
|
storagePath: null,
|
|
});
|
|
|
|
await service.update({ storagePath: '' }, 'user-1');
|
|
|
|
expect(prisma.backupSettings.update).toHaveBeenCalledWith({
|
|
where: { id: 'singleton' },
|
|
data: {
|
|
storagePath: null,
|
|
cronSchedule: undefined,
|
|
retentionCount: undefined,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('leaves fields untouched when omitted from the dto', async () => {
|
|
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
|
prisma.backupSettings.update.mockResolvedValue(existing);
|
|
|
|
await service.update({}, 'user-1');
|
|
|
|
expect(prisma.backupSettings.update).toHaveBeenCalledWith({
|
|
where: { id: 'singleton' },
|
|
data: {
|
|
storagePath: undefined,
|
|
cronSchedule: undefined,
|
|
retentionCount: undefined,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|