Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Sync and data safety - Autosave flushes on leaving a study or the tab; storage-full errors are reported - Unsynced studies, deletes and ink are queued and retried when back online - Deletes propagate across devices (with a "Keep it" undo) and no longer resurrect - Server assigns study versions (no client-clock conflicts); "Combine both" merges a conflicted study chunk by chunk - Reading-plan progress made offline is merged, not overwritten; plans, ink and marks are cleared from the device on sign-out Security - Block SSRF through the ntfy reminder URL; failure-only rate limiting per IP and account; constant-time login; TOTP replay protection; input validation; SESSION_SECRET required in production; admin user delete fixed - Forgot password: one-time code sent to the account's ntfy topic Features - IQ Bible proxy (route allow-list, text-only caching per its terms): more commentaries, lexicons, study aids, daily verse and devotional, plan calendar export, narrations, podcast background - Study templates, reading-plan catch-up, account export, daily database snapshots Tests and tooling - ESLint config; App tests split into src/test; tests for sync queues, library hook, merge, templates, IQ Bible UI and reset form Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
34 lines
1.9 KiB
JavaScript
34 lines
1.9 KiB
JavaScript
// @vitest-environment node
|
|
import { test, expect } from 'vitest';
|
|
import { buildAccountExport, accountExportName, BACKUP_APP } from './accountExport.js';
|
|
import { listSnapshots, pruneSnapshots } from './backups.js';
|
|
import { mkdtempSync, writeFileSync, readdirSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
test('the account export has the same shape as the in-app backup', () => {
|
|
const out = buildAccountExport({ email: 'a@b.co' }, {
|
|
projects: () => [{ id: 'p', title: 'T', chapters: [] }],
|
|
prefs: () => ({ colorLabels: { v: { '#fef08a': 'Promises' }, at: 1 } }),
|
|
marks: () => ({ bookmarks: { 'JHN-3-16': '#fff' }, highlights: [{ id: 'h' }] }),
|
|
ink: () => ({ JHN_3: [[1, 2]] }),
|
|
plans: () => ({ progress: [{ id: 'x' }] }),
|
|
}, new Date('2026-10-06T10:00:00Z'));
|
|
expect(out.app).toBe(BACKUP_APP);
|
|
expect(out.account).toBe('a@b.co');
|
|
expect(out.reader.colorLabels).toEqual({ '#fef08a': 'Promises' });
|
|
expect(out.reader.bookmarks['JHN-3-16']).toBe('#fff');
|
|
expect(out.readingPlans).toEqual({ progress: [{ id: 'x' }], customPlans: [], activity: [] });
|
|
expect(accountExportName(new Date('2026-10-06T10:00:00Z'))).toBe('bible-study-account-2026-10-06.json');
|
|
});
|
|
|
|
test('only the newest snapshots are kept, and always at least one', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'snap-'));
|
|
['2026-10-01-03-00', '2026-10-02-03-00', '2026-10-03-03-00', '2026-10-04-03-00'].forEach((d) => writeFileSync(join(dir, `projects-${d}.db`), 'x'));
|
|
writeFileSync(join(dir, 'notes.txt'), 'not a snapshot');
|
|
expect(listSnapshots(dir).map((s) => s.name)[0]).toBe('projects-2026-10-04-03-00.db');
|
|
expect(pruneSnapshots(2, dir)).toEqual(['projects-2026-10-02-03-00.db', 'projects-2026-10-01-03-00.db']);
|
|
expect(pruneSnapshots(0, dir)).toHaveLength(1); // keep at least the newest
|
|
expect(readdirSync(dir).sort()).toEqual(['notes.txt', 'projects-2026-10-04-03-00.db']);
|
|
});
|