test(18-02): add failing integration tests for admin timezone endpoints
- describe('admin timezone config') covers 8 cases:
- GET and PUT 403 for non-admin authenticated user (T-18-03)
- GET with no stored row returns 200 with isExplicitlySet: false
- PUT America/Chicago then GET round-trip with isExplicitlySet: true
- PUT UTC returns 200 (Pitfall 2)
- PUT Not/AZone returns 400 and does not write to app_config (T-18-04)
- POST seed when unset stores the value (D-02)
- POST seed when already set does NOT overwrite (D-03)
- appConfig imported from db/schema for per-test cleanup
- afterEach removes household_timezone row to prevent test bleed
- 6 new cases FAIL (404 — endpoints not yet implemented); 19 existing pass
This commit is contained in:
@@ -30,7 +30,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db } from '../../src/db/client.js';
|
import { db } from '../../src/db/client.js';
|
||||||
import { users, memberCredentials, calendars } from '../../src/db/schema.js';
|
import { users, memberCredentials, calendars, appConfig } from '../../src/db/schema.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CalDAV mock — intercept createFastmailClient so tests do not hit live Fastmail.
|
// CalDAV mock — intercept createFastmailClient so tests do not hit live Fastmail.
|
||||||
@@ -585,3 +585,171 @@ describe('POST /api/me/credential', () => {
|
|||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// admin timezone config (Plan 18-02: D-01, D-02, D-03, D-04)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
describe('admin timezone config', () => {
|
||||||
|
// Clean up household_timezone between tests to avoid bleed
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.delete(appConfig).where(eq(appConfig.key, 'household_timezone'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// GET /api/admin/config/timezone — access control
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('GET returns 403 for a non-admin authenticated user (T-18-03)', async () => {
|
||||||
|
const nonAdminId = await seedUser('tz-non-admin-get', false);
|
||||||
|
currentDevUserId = nonAdminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
const res = await app.fetch(jsonRequest('GET', '/api/admin/config/timezone'));
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// PUT /api/admin/config/timezone — access control
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('PUT returns 403 for a non-admin authenticated user (T-18-03)', async () => {
|
||||||
|
const nonAdminId = await seedUser('tz-non-admin-put', false);
|
||||||
|
currentDevUserId = nonAdminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
const res = await app.fetch(
|
||||||
|
jsonRequest('PUT', '/api/admin/config/timezone', { timezone: 'America/Chicago' }),
|
||||||
|
);
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// GET /api/admin/config/timezone — admin reads when no row is stored
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('GET as admin with no stored row returns 200 with isExplicitlySet: false and a non-empty timezone', async () => {
|
||||||
|
const adminId = await seedUser('tz-admin-get-default', true);
|
||||||
|
currentDevUserId = adminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
const res = await app.fetch(jsonRequest('GET', '/api/admin/config/timezone'));
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = (await res.json()) as { timezone: string; isExplicitlySet: boolean };
|
||||||
|
expect(typeof body.timezone).toBe('string');
|
||||||
|
expect(body.timezone.length).toBeGreaterThan(0);
|
||||||
|
expect(body.isExplicitlySet).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// PUT then GET round-trip
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('PUT America/Chicago then GET returns that value with isExplicitlySet: true', async () => {
|
||||||
|
const adminId = await seedUser('tz-admin-roundtrip', true);
|
||||||
|
currentDevUserId = adminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
const putRes = await app.fetch(
|
||||||
|
jsonRequest('PUT', '/api/admin/config/timezone', { timezone: 'America/Chicago' }),
|
||||||
|
);
|
||||||
|
expect(putRes.status).toBe(200);
|
||||||
|
|
||||||
|
const getRes = await app.fetch(jsonRequest('GET', '/api/admin/config/timezone'));
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const body = (await getRes.json()) as { timezone: string; isExplicitlySet: boolean };
|
||||||
|
expect(body.timezone).toBe('America/Chicago');
|
||||||
|
expect(body.isExplicitlySet).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// PUT with UTC must succeed (Pitfall 2)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('PUT UTC returns 200 (Pitfall 2 — UTC must be accepted)', async () => {
|
||||||
|
const adminId = await seedUser('tz-admin-utc', true);
|
||||||
|
currentDevUserId = adminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
const res = await app.fetch(
|
||||||
|
jsonRequest('PUT', '/api/admin/config/timezone', { timezone: 'UTC' }),
|
||||||
|
);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// PUT with invalid IANA string returns 400 and writes nothing (T-18-04)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('PUT Not/AZone returns 400 and does not store a value (T-18-04)', async () => {
|
||||||
|
const adminId = await seedUser('tz-admin-invalid', true);
|
||||||
|
currentDevUserId = adminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
const res = await app.fetch(
|
||||||
|
jsonRequest('PUT', '/api/admin/config/timezone', { timezone: 'Not/AZone' }),
|
||||||
|
);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
|
||||||
|
// Verify no row was written to app_config
|
||||||
|
const [row] = await db
|
||||||
|
.select({ value: appConfig.value })
|
||||||
|
.from(appConfig)
|
||||||
|
.where(eq(appConfig.key, 'household_timezone'))
|
||||||
|
.limit(1);
|
||||||
|
expect(row).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// POST /api/admin/config/timezone/seed — seeds when unset (D-02)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('POST seed when unset stores the value and returns ok (D-02)', async () => {
|
||||||
|
const adminId = await seedUser('tz-admin-seed-unset', true);
|
||||||
|
currentDevUserId = adminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
const res = await app.fetch(
|
||||||
|
jsonRequest('POST', '/api/admin/config/timezone/seed', { timezone: 'Europe/London' }),
|
||||||
|
);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
// Verify the value was stored
|
||||||
|
const [row] = await db
|
||||||
|
.select({ value: appConfig.value })
|
||||||
|
.from(appConfig)
|
||||||
|
.where(eq(appConfig.key, 'household_timezone'))
|
||||||
|
.limit(1);
|
||||||
|
expect(row?.value).toBe('Europe/London');
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// POST /api/admin/config/timezone/seed — no-overwrite when already set (D-03)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('POST seed when already set does NOT overwrite the existing value (D-03)', async () => {
|
||||||
|
const adminId = await seedUser('tz-admin-seed-overwrite', true);
|
||||||
|
currentDevUserId = adminId;
|
||||||
|
const app = await getApp();
|
||||||
|
|
||||||
|
// First set a value via PUT
|
||||||
|
const putRes = await app.fetch(
|
||||||
|
jsonRequest('PUT', '/api/admin/config/timezone', { timezone: 'America/Chicago' }),
|
||||||
|
);
|
||||||
|
expect(putRes.status).toBe(200);
|
||||||
|
|
||||||
|
// Now attempt to seed a different value
|
||||||
|
const seedRes = await app.fetch(
|
||||||
|
jsonRequest('POST', '/api/admin/config/timezone/seed', { timezone: 'Asia/Tokyo' }),
|
||||||
|
);
|
||||||
|
expect(seedRes.status).toBe(200);
|
||||||
|
|
||||||
|
// The stored value must still be America/Chicago (no overwrite, D-03)
|
||||||
|
const [row] = await db
|
||||||
|
.select({ value: appConfig.value })
|
||||||
|
.from(appConfig)
|
||||||
|
.where(eq(appConfig.key, 'household_timezone'))
|
||||||
|
.limit(1);
|
||||||
|
expect(row?.value).toBe('America/Chicago');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user