Files
nmemmertandClaude Sonnet 5.5 f274bc9c8a
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Harden sync and security; add IQ Bible, templates, password reset, backups
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>
2026-10-06 08:13:36 -04:00

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']);
});