Files
motovaultpro/backend/src/features/backup/tests/unit/backup-classification.service.test.ts
Eric Gullickson 19ece562ed
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 6m15s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
fix: Implement tiered backup retention classification (refs #6)
Replace per-schedule count-based retention with unified tiered classification.
Backups are now classified by timestamp into categories (hourly/daily/weekly/monthly)
and are only deleted when they exceed ALL applicable category quotas.

Changes:
- Add backup-classification.service.ts for timestamp-based classification
- Rewrite backup-retention.service.ts with tiered logic
- Add categories and expires_at columns to backup_history
- Add Expires column to desktop and mobile backup UI
- Add unit tests for classification logic (22 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:53:43 -06:00

189 lines
6.6 KiB
TypeScript

/**
* @ai-summary Unit tests for BackupClassificationService
* @ai-context Tests pure timestamp-based classification functions
*/
import {
classifyBackup,
calculateExpiration,
isFirstBackupOfDay,
isSunday,
isFirstDayOfMonth,
classifyAndCalculateExpiration,
} from '../../domain/backup-classification.service';
import { TIERED_RETENTION } from '../../domain/backup.types';
describe('BackupClassificationService', () => {
describe('classifyBackup', () => {
it('should classify regular hourly backup (non-midnight)', () => {
// Tuesday, January 7, 2026 at 14:30 UTC
const timestamp = new Date('2026-01-07T14:30:00.000Z');
const categories = classifyBackup(timestamp);
expect(categories).toEqual(['hourly']);
});
it('should classify midnight backup as hourly + daily', () => {
// Wednesday, January 8, 2026 at 00:00 UTC
const timestamp = new Date('2026-01-08T00:00:00.000Z');
const categories = classifyBackup(timestamp);
expect(categories).toEqual(['hourly', 'daily']);
});
it('should classify Sunday midnight backup as hourly + daily + weekly', () => {
// Sunday, January 4, 2026 at 00:00 UTC
const timestamp = new Date('2026-01-04T00:00:00.000Z');
const categories = classifyBackup(timestamp);
expect(categories).toEqual(['hourly', 'daily', 'weekly']);
});
it('should classify 1st of month midnight backup as hourly + daily + monthly', () => {
// Thursday, January 1, 2026 at 00:00 UTC (not Sunday)
const timestamp = new Date('2026-01-01T00:00:00.000Z');
const categories = classifyBackup(timestamp);
expect(categories).toEqual(['hourly', 'daily', 'monthly']);
});
it('should classify Sunday 1st of month midnight as all categories', () => {
// Sunday, February 1, 2026 at 00:00 UTC
const timestamp = new Date('2026-02-01T00:00:00.000Z');
const categories = classifyBackup(timestamp);
expect(categories).toEqual(['hourly', 'daily', 'weekly', 'monthly']);
});
it('should not classify non-midnight on 1st as monthly', () => {
// Thursday, January 1, 2026 at 10:00 UTC
const timestamp = new Date('2026-01-01T10:00:00.000Z');
const categories = classifyBackup(timestamp);
expect(categories).toEqual(['hourly']);
});
it('should not classify non-midnight on Sunday as weekly', () => {
// Sunday, January 4, 2026 at 15:00 UTC
const timestamp = new Date('2026-01-04T15:00:00.000Z');
const categories = classifyBackup(timestamp);
expect(categories).toEqual(['hourly']);
});
});
describe('calculateExpiration', () => {
const baseTimestamp = new Date('2026-01-05T00:00:00.000Z');
it('should calculate 8 hours for hourly-only backup', () => {
const expiresAt = calculateExpiration(['hourly'], baseTimestamp);
const expectedDate = new Date('2026-01-05T08:00:00.000Z');
expect(expiresAt).toEqual(expectedDate);
});
it('should calculate 7 days for daily backup', () => {
const expiresAt = calculateExpiration(['hourly', 'daily'], baseTimestamp);
const expectedDate = new Date('2026-01-12T00:00:00.000Z');
expect(expiresAt).toEqual(expectedDate);
});
it('should calculate 4 weeks for weekly backup', () => {
const expiresAt = calculateExpiration(['hourly', 'daily', 'weekly'], baseTimestamp);
const expectedDate = new Date('2026-02-02T00:00:00.000Z');
expect(expiresAt).toEqual(expectedDate);
});
it('should calculate 12 months for monthly backup', () => {
const expiresAt = calculateExpiration(
['hourly', 'daily', 'weekly', 'monthly'],
baseTimestamp
);
const expectedDate = new Date('2027-01-05T00:00:00.000Z');
expect(expiresAt).toEqual(expectedDate);
});
it('should use longest retention when monthly is present (even without weekly)', () => {
const expiresAt = calculateExpiration(['hourly', 'daily', 'monthly'], baseTimestamp);
const expectedDate = new Date('2027-01-05T00:00:00.000Z');
expect(expiresAt).toEqual(expectedDate);
});
});
describe('isFirstBackupOfDay', () => {
it('should return true for midnight UTC', () => {
const timestamp = new Date('2026-01-05T00:00:00.000Z');
expect(isFirstBackupOfDay(timestamp)).toBe(true);
});
it('should return false for non-midnight', () => {
const timestamp = new Date('2026-01-05T01:00:00.000Z');
expect(isFirstBackupOfDay(timestamp)).toBe(false);
});
it('should return true for midnight with minutes/seconds', () => {
// 00:30:45 is still hour 0
const timestamp = new Date('2026-01-05T00:30:45.000Z');
expect(isFirstBackupOfDay(timestamp)).toBe(true);
});
});
describe('isSunday', () => {
it('should return true for Sunday', () => {
// January 4, 2026 is a Sunday
const timestamp = new Date('2026-01-04T12:00:00.000Z');
expect(isSunday(timestamp)).toBe(true);
});
it('should return false for non-Sunday', () => {
// January 5, 2026 is a Monday
const timestamp = new Date('2026-01-05T12:00:00.000Z');
expect(isSunday(timestamp)).toBe(false);
});
});
describe('isFirstDayOfMonth', () => {
it('should return true for 1st of month', () => {
const timestamp = new Date('2026-01-01T12:00:00.000Z');
expect(isFirstDayOfMonth(timestamp)).toBe(true);
});
it('should return false for non-1st', () => {
const timestamp = new Date('2026-01-15T12:00:00.000Z');
expect(isFirstDayOfMonth(timestamp)).toBe(false);
});
});
describe('classifyAndCalculateExpiration', () => {
it('should return both categories and expiresAt', () => {
// Sunday, February 1, 2026 at 00:00 UTC - all categories
const timestamp = new Date('2026-02-01T00:00:00.000Z');
const result = classifyAndCalculateExpiration(timestamp);
expect(result.categories).toEqual(['hourly', 'daily', 'weekly', 'monthly']);
expect(result.expiresAt).toEqual(new Date('2027-02-01T00:00:00.000Z'));
});
it('should work for hourly-only backup', () => {
const timestamp = new Date('2026-01-07T14:30:00.000Z');
const result = classifyAndCalculateExpiration(timestamp);
expect(result.categories).toEqual(['hourly']);
expect(result.expiresAt).toEqual(new Date('2026-01-07T22:30:00.000Z'));
});
});
describe('TIERED_RETENTION constants', () => {
it('should have correct retention values', () => {
expect(TIERED_RETENTION.hourly).toBe(8);
expect(TIERED_RETENTION.daily).toBe(7);
expect(TIERED_RETENTION.weekly).toBe(4);
expect(TIERED_RETENTION.monthly).toBe(12);
});
});
});