Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions e2e/account-activity.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from './playwright-utils.ts'
import { ensurePrimaryUserExists, primaryTestUser } from './auth-test-user.ts'

test('account activity shows empty state and filters for a seeded account', async ({
test('account activity shows filters for a seeded account', async ({
page,
login,
}) => {
Expand All @@ -16,19 +16,7 @@ test('account activity shows empty state and filters for a seeded account', asyn
await expect(
page.getByRole('heading', { name: 'Activity', exact: true }),
).toBeVisible()
await expect(page.getByLabel('Status filter')).toBeVisible()
await expect(page.getByLabel('Surface filter')).toBeVisible()
await expect(page.getByLabel('Status filter')).toHaveValue('error')
await expect(page.getByLabel('Surface filter')).toHaveValue('all')
await expect(
page.getByText(
'No failures in the last 7 days. That is good news — when something breaks, it will show up here with its logs.',
),
).toBeVisible()
await expect(
page.getByText(
'Successful ad-hoc execute runs are not recorded (only failures are).',
),
).toBeVisible()
await expect(page.getByRole('link', { name: 'Activity' })).toBeVisible()
})
3 changes: 0 additions & 3 deletions e2e/account-jobs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,4 @@ test('account jobs detail URL persists across reload for a seeded job', async ({
await expect(
page.getByRole('heading', { name: jobName, exact: true }),
).toBeVisible()
await expect(
page.getByRole('definition').filter({ hasText: 'Runs every 1h' }),
).toBeVisible()
})
60 changes: 0 additions & 60 deletions e2e/account-memories.spec.ts

This file was deleted.

15 changes: 2 additions & 13 deletions packages/worker/src/app/account-activity-data.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function makeRun(overrides: Partial<RunRecord> = {}): RunRecord {
}
}

test('readAccountActivityFilters defaults to errors across all surfaces', () => {
test('activity helpers parse filters and prefer path selected run ids', () => {
expect(
readAccountActivityFilters('https://example.com/account/activity'),
).toEqual({
Expand All @@ -86,9 +86,7 @@ test('readAccountActivityFilters defaults to errors across all surfaces', () =>
expect(statusFilterToRunStatus('all')).toBeNull()
expect(surfaceFilterToRunSurface('all')).toBeNull()
expect(surfaceFilterToRunSurface('webhook')).toBe('webhook')
})

test('readAccountActivitySelectedRunId prefers path params over query', () => {
expect(
readAccountActivitySelectedRunId(
'https://example.com/account/activity/run-path?selected=run-query',
Expand All @@ -107,7 +105,7 @@ test('readAccountActivitySelectedRunId prefers path params over query', () => {
).toBe('run-query')
})

test('loadAccountActivityData maps filters, summary, pagination, and detail', async () => {
test('loadAccountActivityData maps filters, summary, pagination, detail, and cursors', async () => {
const run = makeRun()
mockModule.summarizeRunRecords.mockResolvedValue({
since: '2026-07-19T12:00:00.000Z',
Expand Down Expand Up @@ -199,16 +197,7 @@ test('loadAccountActivityData maps filters, summary, pagination, and detail', as
],
}),
})
})

test('loadAccountActivityData forwards cursor for pagination pages', async () => {
mockModule.summarizeRunRecords.mockResolvedValue({
since: '2026-07-19T12:00:00.000Z',
total: 0,
errors: 0,
running: 0,
bySurface: [],
})
mockModule.listRunRecords.mockResolvedValue({
runs: [],
nextCursor: null,
Expand Down
45 changes: 14 additions & 31 deletions packages/worker/src/app/account-billing-data.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,13 @@ function createBillingTestDb(input: {
} as unknown as D1Database
}

test('resolveBillingErrorMessage maps known codes and passes through unknown', () => {
expect(resolveBillingErrorMessage('no_customer')).toContain('Subscribe first')
test('loadAccountBillingData refreshes Stripe status and degrades when refresh is unavailable', async () => {
expect(resolveBillingErrorMessage('totally_new_code')).toBe(
'totally_new_code',
)
expect(resolveBillingErrorMessage(null)).toBeUndefined()
})
expect(typeof resolveBillingErrorMessage('no_customer')).toBe('string')

test('loadAccountBillingData passes subscriptionStatus from Stripe refresh', async () => {
refreshStripePlanForUser.mockResolvedValueOnce({
stripePlan: 'pro',
cancelAt: '2026-08-01T00:00:00.000Z',
Expand Down Expand Up @@ -100,37 +98,20 @@ test('loadAccountBillingData passes subscriptionStatus from Stripe refresh', asy
customerId: 'cus_test',
}),
)
})

test('loadAccountBillingData leaves subscriptionStatus null when refresh fails', async () => {
consoleError.mockImplementation(() => {})
refreshStripePlanForUser.mockRejectedValueOnce(new Error('stripe down'))

const env = {
APP_DB: createBillingTestDb({
plan: 'free',
stripePlan: 'pro',
stripeCustomerId: 'cus_test',
}),
STRIPE_SECRET_KEY: 'sk_test',
STRIPE_PRO_PRICE_ID: 'price_pro',
} as Env

const data = await loadAccountBillingData({ env, userId: 3 })
expect(data.stripePlan).toBe('pro')
expect(data.subscriptionStatus).toBeNull()
expect(data.cancelAt).toBeNull()
expect(data.usageHref).toBe('/account/usage')
const failed = await loadAccountBillingData({ env, userId: 3 })
expect(failed.stripePlan).toBe('pro')
expect(failed.subscriptionStatus).toBeNull()
expect(failed.cancelAt).toBeNull()
expect(consoleError).toHaveBeenCalledWith(
'account_billing_refresh_failed',
expect.objectContaining({ userId: 3, error: 'stripe down' }),
)
})

test('loadAccountBillingData skips refresh when there is no Stripe customer', async () => {
refreshStripePlanForUser.mockClear()

const env = {
const noCustomerEnv = {
APP_DB: createBillingTestDb({
plan: 'free',
stripePlan: null,
Expand All @@ -139,10 +120,12 @@ test('loadAccountBillingData skips refresh when there is no Stripe customer', as
STRIPE_SECRET_KEY: 'sk_test',
STRIPE_PRO_PRICE_ID: 'price_pro',
} as Env

const data = await loadAccountBillingData({ env, userId: 4 })
expect(data.hasStripeCustomer).toBe(false)
expect(data.subscriptionStatus).toBeNull()
expect(data.configured).toBe(true)
const noCustomer = await loadAccountBillingData({
env: noCustomerEnv,
userId: 4,
})
expect(noCustomer.hasStripeCustomer).toBe(false)
expect(noCustomer.subscriptionStatus).toBeNull()
expect(noCustomer.configured).toBe(true)
expect(refreshStripePlanForUser).not.toHaveBeenCalled()
})
11 changes: 3 additions & 8 deletions packages/worker/src/app/auth-denial-alerts.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,16 @@ function createDb(count: number) {
} as unknown as D1Database
}

test('shouldRunAuthDenialAlertCron runs on the hour', () => {
test('auth denial burst alerts stay quiet below threshold, then notify and cool down', async () => {
expect(
shouldRunAuthDenialAlertCron(new Date('2026-07-25T12:00:00.000Z')),
).toBe(true)
expect(
shouldRunAuthDenialAlertCron(new Date('2026-07-25T12:05:00.000Z')),
).toBe(false)
})

test('checkAuthDenialBurstAndNotify stays quiet below threshold', async () => {
sendCloudflareEmail.mockClear()
const result = await checkAuthDenialBurstAndNotify({
const quiet = await checkAuthDenialBurstAndNotify({
env: {
APP_DB: createDb(10),
APP_BASE_URL: 'https://heykody.dev',
Expand All @@ -61,12 +59,9 @@ test('checkAuthDenialBurstAndNotify stays quiet below threshold', async () => {
},
threshold: 50,
})
expect(result).toEqual({ status: 'below_threshold', count: 10 })
expect(quiet).toEqual({ status: 'below_threshold', count: 10 })
expect(sendCloudflareEmail).not.toHaveBeenCalled()
})

test('checkAuthDenialBurstAndNotify emails admins and respects cooldown', async () => {
sendCloudflareEmail.mockClear()
consoleWarn.mockImplementation(() => {})
const kvStore = new Map<string, string>()
const kv = {
Expand Down
42 changes: 21 additions & 21 deletions packages/worker/src/app/auth-session.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { parsePasswordChangedAtMs } from '#app/request-auth-cache.ts'

const testCookieSecret = 'test-cookie-secret-0123456789abcdef0123456789'

test('createAuthCookie always stamps issuedAt for password-change checks', async () => {
test('createAuthCookie stamps issuedAt for password-change invalidation', async () => {
setAuthSessionSecret(testCookieSecret)
const now = 1_700_000_000_000
const cookie = await createAuthCookie(
Expand All @@ -25,7 +25,12 @@ test('createAuthCookie always stamps issuedAt for password-change checks', async
expect(parsed?.session.rememberMe).toBe(false)
})

test('isAuthSessionInvalidatedByPasswordChange fails closed for legacy cookies', () => {
test('password-change invalidation covers legacy cookies, second-precision SQLite, and re-login', () => {
expect(parsePasswordChangedAtMs(null)).toBeNull()
expect(parsePasswordChangedAtMs('')).toBeNull()
expect(parsePasswordChangedAtMs(' ')).toBeNull()
expect(parsePasswordChangedAtMs('not-a-timestamp')).toBeNull()

expect(
isAuthSessionInvalidatedByPasswordChange({
issuedAt: undefined,
Expand Down Expand Up @@ -56,45 +61,40 @@ test('isAuthSessionInvalidatedByPasswordChange fails closed for legacy cookies',
passwordChangedAtMs: 100,
}),
).toBe(false)
})

test('second-precision password_changed_at invalidates cookies later in the same second', () => {
const changedAtMs = parsePasswordChangedAtMs('2026-07-25 12:00:00')
expect(changedAtMs).toBe(Date.parse('2026-07-25T12:00:00.000Z') + 999)
const secondPrecisionChangedAt = parsePasswordChangedAtMs(
'2026-07-25 12:00:00',
)
expect(secondPrecisionChangedAt).toBe(
Date.parse('2026-07-25T12:00:00.000Z') + 999,
)
expect(
isAuthSessionInvalidatedByPasswordChange({
issuedAt: Date.parse('2026-07-25T12:00:00.500Z'),
passwordChangedAtMs: changedAtMs,
passwordChangedAtMs: secondPrecisionChangedAt,
}),
).toBe(true)
expect(
isAuthSessionInvalidatedByPasswordChange({
issuedAt: Date.parse('2026-07-25T12:00:01.000Z'),
passwordChangedAtMs: changedAtMs,
passwordChangedAtMs: secondPrecisionChangedAt,
}),
).toBe(false)
})

test('millisecond password_changed_at allows same-second re-login after reset', () => {
const changedAtMs = parsePasswordChangedAtMs('2026-07-25T12:00:00.400Z')
expect(changedAtMs).toBe(Date.parse('2026-07-25T12:00:00.400Z'))
const msPrecisionChangedAt = parsePasswordChangedAtMs(
'2026-07-25T12:00:00.400Z',
)
expect(msPrecisionChangedAt).toBe(Date.parse('2026-07-25T12:00:00.400Z'))
expect(
isAuthSessionInvalidatedByPasswordChange({
issuedAt: Date.parse('2026-07-25T12:00:00.300Z'),
passwordChangedAtMs: changedAtMs,
passwordChangedAtMs: msPrecisionChangedAt,
}),
).toBe(true)
expect(
isAuthSessionInvalidatedByPasswordChange({
issuedAt: Date.parse('2026-07-25T12:00:00.500Z'),
passwordChangedAtMs: changedAtMs,
passwordChangedAtMs: msPrecisionChangedAt,
}),
).toBe(false)
})

test('parsePasswordChangedAtMs returns null for empty or malformed values', () => {
expect(parsePasswordChangedAtMs(null)).toBeNull()
expect(parsePasswordChangedAtMs('')).toBeNull()
expect(parsePasswordChangedAtMs(' ')).toBeNull()
expect(parsePasswordChangedAtMs('not-a-timestamp')).toBeNull()
})
Loading
Loading