Files
familysync/apps/api/tests/lib/householdTimezone.test.ts
T
Lucas Berger db0077c3c3 test(18-01): add failing tests for household timezone accessor + IANA validator
- RED gate: tests for getHouseholdTimezone fallback chain (stored → TZ env → Intl)
- Tests for null row value falling through to TZ env branch
- Tests for isValidIanaTimezone (UTC, Etc/UTC, America/Chicago, Europe/London pass; garbage fails)
- Mock Drizzle select chain follows requireAdmin.test.ts pattern
- Saves/restores process.env.TZ in beforeEach/afterEach to prevent env state leaks
2026-06-14 22:05:58 -04:00

131 lines
4.5 KiB
TypeScript

/**
* householdTimezone unit tests (Plan 18-01)
*
* Behavior-pinned contracts:
* 1. getHouseholdTimezone: DB row present → returns stored value
* 2. getHouseholdTimezone: no DB row, process.env.TZ set → returns TZ value
* 3. getHouseholdTimezone: no DB row, no TZ env var → returns Intl.DateTimeFormat().resolvedOptions().timeZone
* 4. getHouseholdTimezone: DB row present but value is null → falls through to env.TZ branch
* 5. isValidIanaTimezone: returns true for valid IANA zones ('UTC', 'Etc/UTC', 'America/Chicago', 'Europe/London')
* 6. isValidIanaTimezone: returns false for garbage ('Not/AZone', '', 'Mars/Phobos')
*
* Uses a mocked Drizzle db chain (same pattern as requireAdmin.test.ts).
* Saves and restores process.env.TZ around each test to avoid leaking env state.
*
* Run: pnpm --filter @familysync/api exec vitest run tests/lib/householdTimezone.test.ts
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// Mock the db singleton so tests do not need a live MariaDB connection.
vi.mock('../../src/db/client.js', () => ({
db: {
select: vi.fn(),
},
}));
import { db } from '../../src/db/client.js';
import { getHouseholdTimezone, isValidIanaTimezone } from '../../src/lib/householdTimezone.js';
const mockDb = db as { select: ReturnType<typeof vi.fn> };
// ── DB query chain builder ────────────────────────────────────────────────────
/**
* Builds a Drizzle select() → from() → where() → limit() chain
* that resolves to the given array.
*/
function makeSelectChain(resolvedValue: unknown[]) {
const chain = {
from: vi.fn(),
where: vi.fn(),
limit: vi.fn().mockResolvedValue(resolvedValue),
};
chain.from.mockReturnValue(chain);
chain.where.mockReturnValue(chain);
return chain;
}
// ── Test suite ────────────────────────────────────────────────────────────────
describe('getHouseholdTimezone', () => {
let originalTZ: string | undefined;
beforeEach(() => {
vi.clearAllMocks();
// Save the current TZ env value before each test
originalTZ = process.env.TZ;
});
afterEach(() => {
// Restore TZ to its original value (or delete if it was unset)
if (originalTZ === undefined) {
delete process.env.TZ;
} else {
process.env.TZ = originalTZ;
}
});
it('returns the stored value when the DB has a row for household_timezone', async () => {
mockDb.select.mockReturnValue(makeSelectChain([{ value: 'America/Chicago' }]));
const result = await getHouseholdTimezone(mockDb as never);
expect(result).toBe('America/Chicago');
});
it('falls back to process.env.TZ when the DB returns no row', async () => {
mockDb.select.mockReturnValue(makeSelectChain([]));
process.env.TZ = 'America/New_York';
const result = await getHouseholdTimezone(mockDb as never);
expect(result).toBe('America/New_York');
});
it('falls back to Intl.DateTimeFormat().resolvedOptions().timeZone when no DB row and TZ is unset', async () => {
mockDb.select.mockReturnValue(makeSelectChain([]));
delete process.env.TZ;
const expected = Intl.DateTimeFormat().resolvedOptions().timeZone;
const result = await getHouseholdTimezone(mockDb as never);
expect(result).toBe(expected);
});
it('falls through to process.env.TZ when the DB row value is null', async () => {
mockDb.select.mockReturnValue(makeSelectChain([{ value: null }]));
process.env.TZ = 'Europe/London';
const result = await getHouseholdTimezone(mockDb as never);
expect(result).toBe('Europe/London');
});
});
describe('isValidIanaTimezone', () => {
it("returns true for 'UTC'", () => {
expect(isValidIanaTimezone('UTC')).toBe(true);
});
it("returns true for 'Etc/UTC'", () => {
expect(isValidIanaTimezone('Etc/UTC')).toBe(true);
});
it("returns true for 'America/Chicago'", () => {
expect(isValidIanaTimezone('America/Chicago')).toBe(true);
});
it("returns true for 'Europe/London'", () => {
expect(isValidIanaTimezone('Europe/London')).toBe(true);
});
it("returns false for 'Not/AZone'", () => {
expect(isValidIanaTimezone('Not/AZone')).toBe(false);
});
it("returns false for empty string ''", () => {
expect(isValidIanaTimezone('')).toBe(false);
});
it("returns false for 'Mars/Phobos'", () => {
expect(isValidIanaTimezone('Mars/Phobos')).toBe(false);
});
});