aboutsummaryrefslogtreecommitdiff
path: root/src/lib/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/server')
-rw-r--r--src/lib/server/auth.test.ts18
-rw-r--r--src/lib/server/auth.ts17
-rw-r--r--src/lib/server/db/groceries.test.ts12
-rw-r--r--src/lib/server/db/ondelete.test.ts4
-rw-r--r--src/lib/server/services/categories.test.ts4
-rw-r--r--src/lib/server/services/groceries.test.ts20
-rw-r--r--src/lib/server/services/groceries.ts4
-rw-r--r--src/lib/server/services/references.test.ts12
-rw-r--r--src/lib/server/services/routine-actions.ts2
-rw-r--r--src/lib/server/services/routines.test.ts33
-rw-r--r--src/lib/server/services/routines.ts12
11 files changed, 94 insertions, 44 deletions
diff --git a/src/lib/server/auth.test.ts b/src/lib/server/auth.test.ts
index 59ddabc..d19aa03 100644
--- a/src/lib/server/auth.test.ts
+++ b/src/lib/server/auth.test.ts
@@ -12,7 +12,7 @@ import {
const TEST_PASSWORD = vi.hoisted(() => {
return 'family-password-5f2c1d7e';
});
-const NOW = 1_800_000_000_000;
+const NOW = Temporal.Instant.fromEpochMilliseconds(1_800_000_000_000);
vi.mock('$env/dynamic/private', () => ({
env: { AUTH_PASSWORD: TEST_PASSWORD },
@@ -27,12 +27,12 @@ describe('authentication', () => {
it('accepts a signed session until its expiry', () => {
const session = createSession(NOW);
- expect(
- isValidSession(session, NOW + SESSION_MAX_AGE_SECONDS * 1000 - 1),
- ).toBe(true);
- expect(isValidSession(session, NOW + SESSION_MAX_AGE_SECONDS * 1000)).toBe(
- false,
- );
+ const almostExpired = NOW.add({
+ milliseconds: SESSION_MAX_AGE_SECONDS * 1000 - 1,
+ });
+ expect(isValidSession(session, almostExpired)).toBe(true);
+ const expired = NOW.add({ milliseconds: SESSION_MAX_AGE_SECONDS * 1000 });
+ expect(isValidSession(session, expired)).toBe(false);
expect(isValidSession(`${session}x`, NOW)).toBe(false);
});
@@ -43,7 +43,9 @@ describe('authentication', () => {
set: vi.fn(),
} as unknown as Cookies;
- expect(renewSession(cookies, true, NOW + 1_000)).toBe(true);
+ expect(renewSession(cookies, true, NOW.add({ milliseconds: 1_000 }))).toBe(
+ true,
+ );
expect(cookies.set).toHaveBeenCalledWith(
AUTH_COOKIE_NAME,
expect.any(String),
diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts
index f80eda0..3db93f0 100644
--- a/src/lib/server/auth.ts
+++ b/src/lib/server/auth.ts
@@ -30,15 +30,19 @@ export function verifyPassword(candidate: string): boolean {
return timingSafeEqual(actual, expected);
}
-export function createSession(now = Date.now()): string {
- const expiresAt = now + SESSION_MAX_AGE_MS;
+export function createSession(
+ now: Temporal.Instant = Temporal.Now.instant(),
+): string {
+ const expiresAt = now.add({
+ milliseconds: SESSION_MAX_AGE_MS,
+ }).epochMilliseconds;
const payload = String(expiresAt);
return `${payload}.${sign(payload)}`;
}
export function isValidSession(
value: string | undefined,
- now = Date.now(),
+ now: Temporal.Instant = Temporal.Now.instant(),
): boolean {
if (!value) return false;
@@ -46,7 +50,8 @@ export function isValidSession(
if (!expiresAtText || !signature || extra) return false;
const expiresAt = Number(expiresAtText);
- if (!Number.isSafeInteger(expiresAt) || expiresAt <= now) return false;
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= now.epochMilliseconds)
+ return false;
const expected = Buffer.from(sign(expiresAtText), 'base64url');
const actual = Buffer.from(signature, 'base64url');
@@ -58,7 +63,7 @@ export function isValidSession(
export function renewSession(
cookies: Cookies,
secure: boolean,
- now = Date.now(),
+ now: Temporal.Instant = Temporal.Now.instant(),
): boolean {
const current = cookies.get(AUTH_COOKIE_NAME);
if (!isValidSession(current, now)) return false;
@@ -76,7 +81,7 @@ export function renewSession(
export function setSession(
cookies: Cookies,
secure: boolean,
- now = Date.now(),
+ now: Temporal.Instant = Temporal.Now.instant(),
): void {
cookies.set(AUTH_COOKIE_NAME, createSession(now), {
path: '/',
diff --git a/src/lib/server/db/groceries.test.ts b/src/lib/server/db/groceries.test.ts
index 143e4da..7fec40b 100644
--- a/src/lib/server/db/groceries.test.ts
+++ b/src/lib/server/db/groceries.test.ts
@@ -29,7 +29,9 @@ describe('groceries add', () => {
await db.insert(groceries).values({
name,
categoryId,
- createdAt: new Date().toISOString(),
+ createdAt: Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ }),
});
}
}
@@ -44,7 +46,9 @@ describe('groceries add', () => {
});
it('unchecks an existing checked item instead of creating duplicate', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db
.insert(groceries)
.values({ name: 'milk', checked: true, createdAt: now });
@@ -56,7 +60,9 @@ describe('groceries add', () => {
});
it('keeps an unchecked item unchanged (already on list)', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db
.insert(groceries)
.values({ name: 'milk', checked: false, createdAt: now });
diff --git a/src/lib/server/db/ondelete.test.ts b/src/lib/server/db/ondelete.test.ts
index 347bb92..5dcf69e 100644
--- a/src/lib/server/db/ondelete.test.ts
+++ b/src/lib/server/db/ondelete.test.ts
@@ -18,7 +18,9 @@ describe('category delete behavior', () => {
await db.insert(groceries).values({
name: 'lait',
checked: false,
- createdAt: new Date().toISOString(),
+ createdAt: Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ }),
categoryId: catId,
});
diff --git a/src/lib/server/services/categories.test.ts b/src/lib/server/services/categories.test.ts
index a89b816..dbb21fb 100644
--- a/src/lib/server/services/categories.test.ts
+++ b/src/lib/server/services/categories.test.ts
@@ -31,7 +31,9 @@ describe('categories service', () => {
.insert(categories)
.values({ name: 'Dairy' })
.returning();
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db.insert(groceries).values({
name: 'milk',
checked: false,
diff --git a/src/lib/server/services/groceries.test.ts b/src/lib/server/services/groceries.test.ts
index a704b57..4d9aa2f 100644
--- a/src/lib/server/services/groceries.test.ts
+++ b/src/lib/server/services/groceries.test.ts
@@ -18,7 +18,9 @@ describe('groceries service', () => {
});
it('loadUnchecked returns only unchecked items', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db.insert(groceries).values([
{ name: 'milk', checked: false, createdAt: now },
{ name: 'bread', checked: true, createdAt: now },
@@ -30,7 +32,9 @@ describe('groceries service', () => {
});
it('loadAll returns all items', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db.insert(groceries).values([
{ name: 'milk', checked: false, createdAt: now },
{ name: 'bread', checked: true, createdAt: now },
@@ -41,7 +45,9 @@ describe('groceries service', () => {
});
it('toggle flips checked state', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db
.insert(groceries)
.values({ name: 'milk', checked: false, createdAt: now });
@@ -67,7 +73,9 @@ describe('groceries service', () => {
});
it('addOrUncheck unchecks an existing checked item', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db
.insert(groceries)
.values({ name: 'milk', checked: true, createdAt: now });
@@ -83,7 +91,9 @@ describe('groceries service', () => {
});
it('deleteGrocery removes the item', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
await db
.insert(groceries)
.values({ name: 'milk', checked: false, createdAt: now });
diff --git a/src/lib/server/services/groceries.ts b/src/lib/server/services/groceries.ts
index 8ffcde3..9aa8102 100644
--- a/src/lib/server/services/groceries.ts
+++ b/src/lib/server/services/groceries.ts
@@ -96,7 +96,9 @@ export async function addOrUncheck(
.values({
name,
categoryId,
- createdAt: new Date().toISOString(),
+ createdAt: Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ }),
})
.returning();
diff --git a/src/lib/server/services/references.test.ts b/src/lib/server/services/references.test.ts
index 1ec3fdb..49c5c94 100644
--- a/src/lib/server/services/references.test.ts
+++ b/src/lib/server/services/references.test.ts
@@ -12,7 +12,9 @@ describe('references service', () => {
});
it('loadForGrocery returns refs for the given grocery', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
const [g1] = await db
.insert(groceries)
.values({ name: 'coffee', checked: false, createdAt: now })
@@ -33,7 +35,9 @@ describe('references service', () => {
});
it('add creates a reference', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
const [g] = await db
.insert(groceries)
.values({ name: 'pasta', checked: false, createdAt: now })
@@ -51,7 +55,9 @@ describe('references service', () => {
});
it('deleteReference removes the ref', async () => {
- const now = new Date().toISOString();
+ const now = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
const [g] = await db
.insert(groceries)
.values({ name: 'rice', checked: false, createdAt: now })
diff --git a/src/lib/server/services/routine-actions.ts b/src/lib/server/services/routine-actions.ts
index b546ec7..64d0a1b 100644
--- a/src/lib/server/services/routine-actions.ts
+++ b/src/lib/server/services/routine-actions.ts
@@ -68,7 +68,7 @@ export async function completeRoutineAction(data: FormData, routineId: number) {
await complete(
routineId,
parseFormString(data.get('note')),
- new Date().toISOString(),
+ Temporal.Now.instant().toString({ smallestUnit: 'millisecond' }),
);
} catch {
return fail(400, { error: 'Routine introuvable' });
diff --git a/src/lib/server/services/routines.test.ts b/src/lib/server/services/routines.test.ts
index 22b49ff..eda8824 100644
--- a/src/lib/server/services/routines.test.ts
+++ b/src/lib/server/services/routines.test.ts
@@ -12,8 +12,6 @@ import {
loadOverview,
} from './routines';
-const DAY_MS = 24 * 60 * 60 * 1000;
-
describe('routines service', () => {
let db: Awaited<ReturnType<typeof createTestDb>>['db'];
let projectId: number;
@@ -84,7 +82,11 @@ describe('routines service', () => {
expect(versions[0].archived).toBe(false);
expect(versions[1].archived).toBe(true);
- const dashboard = await loadProjectDashboard(projectId, Date.now(), db);
+ const dashboard = await loadProjectDashboard(
+ projectId,
+ Temporal.Now.instant(),
+ db,
+ );
expect(dashboard.routines).toHaveLength(0);
expect(dashboard.healthScore).toBe(100);
});
@@ -116,15 +118,19 @@ describe('routines service', () => {
const archived = await loadArchived(projectId, db);
expect(archived).toHaveLength(2);
// Descending archive date; equal timestamps keep insertion order.
- expect(new Date(archived[0].archivedAt).getTime()).toBeGreaterThanOrEqual(
- new Date(archived[1].archivedAt).getTime(),
+ expect(
+ Temporal.Instant.from(archived[0].archivedAt).epochMilliseconds,
+ ).toBeGreaterThanOrEqual(
+ Temporal.Instant.from(archived[1].archivedAt).epochMilliseconds,
);
});
it('complete records the snapshot version it was made against', async () => {
const r = await add(projectId, 'clean kitchen', 7, db);
await edit(r.id, { frequencyDays: 5 }, db);
- const doneAt = new Date().toISOString();
+ const doneAt = Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ });
const completion = await complete(r.id, 'with vinegar', doneAt, db);
expect(completion.routineVersion).toBe(2);
expect(completion.note).toBe('with vinegar');
@@ -132,16 +138,21 @@ describe('routines service', () => {
it('dashboard computes urgency and health from completions', async () => {
const r = await add(projectId, 'clean kitchen', 7, db);
- const createdAt = Date.parse(r.createdAt);
- await complete(r.id, null, new Date(createdAt).toISOString(), db);
+ const created = Temporal.Instant.from(r.createdAt);
+ await complete(
+ r.id,
+ null,
+ created.toString({ smallestUnit: 'millisecond' }),
+ db,
+ );
// 22 days after completion with a 7-day routine => two periods overdue
- const now = createdAt + 22 * DAY_MS;
+ const now = created.toZonedDateTimeISO('UTC').add({ days: 22 }).toInstant();
const dashboard = await loadProjectDashboard(projectId, now, db);
expect(dashboard.routines).toHaveLength(1);
expect(dashboard.routines[0].status).toBe('veryLate');
expect(dashboard.healthScore).toBe(0);
const last = dashboard.healthSeries[dashboard.healthSeries.length - 1];
- expect(last.at).toBe(now);
+ expect(last.at).toBe(now.epochMilliseconds);
expect(last.health).toBe(dashboard.healthScore);
});
@@ -150,7 +161,7 @@ describe('routines service', () => {
const growth = await addProject('Personal growth', db);
await add(growth.id, 'read 30 minutes', 1, db);
- const now = Date.now();
+ const now = Temporal.Now.instant();
const overview = await loadOverview(now, db);
expect(overview.projects).toHaveLength(2);
expect(overview.globalScore).toBe(
diff --git a/src/lib/server/services/routines.ts b/src/lib/server/services/routines.ts
index ec03c50..a29872a 100644
--- a/src/lib/server/services/routines.ts
+++ b/src/lib/server/services/routines.ts
@@ -36,7 +36,9 @@ export async function add(
projectId,
name,
frequencyDays,
- createdAt: new Date().toISOString(),
+ createdAt: Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ }),
})
.returning();
return inserted;
@@ -91,7 +93,9 @@ export async function archive(id: number, db = defaultDb): Promise<void> {
...current,
version: current.version + 1,
archived: true,
- archivedAt: new Date().toISOString(),
+ archivedAt: Temporal.Now.instant().toString({
+ smallestUnit: 'millisecond',
+ }),
});
}
@@ -206,7 +210,7 @@ export interface ProjectDashboard {
export async function loadProjectDashboard(
projectId: number,
- now: number,
+ now: Temporal.Instant,
db = defaultDb,
): Promise<ProjectDashboard> {
const versions = await db
@@ -249,7 +253,7 @@ export interface ProjectsOverview {
}
export async function loadOverview(
- now: number,
+ now: Temporal.Instant,
db = defaultDb,
): Promise<ProjectsOverview> {
const activeProjects = await db