feat(auth): add dynamic OAuth provider system (#282)

* feat(auth): add dynamic OAuth provider system

Admin can configure OAuth/OIDC providers in the database via API.
All 35 built-in better-auth providers are registered as async functions
that read config from system_options at runtime. Custom OIDC providers
use the genericOAuth plugin with configs loaded at auth init time.

New endpoints:
- GET /api/auth-providers (public, enabled only, no secrets)
- GET /api/auth-providers/admin (admin, all configs, masked secrets)
- PUT /api/auth-providers/admin/:providerId (admin, upsert)
- DELETE /api/auth-providers/admin/:providerId (admin, remove)

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: async createTestApp compat in email and invite test files

createAuth became async in the OAuth PR, which made createTestApp async.
Email and invite code test files need await + Awaited<> type wrappers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jasper Van
2026-04-12 21:46:11 -04:00
committed by GitHub
parent 97e30a507e
commit 4a5fbb7f4a
31 changed files with 954 additions and 349 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ const platform = isD1 ? resetD1() : resetNode()
// ── 2. seed ──
const secret = process.env.BETTER_AUTH_SECRET || 'dev-secret-for-seed'
const auth = createAuth(platform.db, secret, 'http://localhost:8222')
const auth = await createAuth(platform.db, secret, 'http://localhost:8222')
const app = createApp(platform, auth)
// register admin user
+3
View File
@@ -6,6 +6,7 @@ import { accessLog } from './middleware/logger'
import type { Env } from './middleware/platform'
import { platformMiddleware } from './middleware/platform'
import type { Platform } from './platform/interface'
import authProviders from './routes/auth-providers'
import emailConfig from './routes/email-config'
import { adminInviteCodes, publicInviteCodes } from './routes/invite-codes'
import objects from './routes/objects'
@@ -50,6 +51,7 @@ export function createApp(platform: Platform, auth: Auth) {
app.route('/api/admin/quotas', adminQuotas)
app.route('/api/quotas', userQuotas)
app.route('/api/system', system)
app.route('/api/auth-providers', authProviders)
app.get('/api/health', (c) => c.json({ status: 'ok' }))
@@ -69,3 +71,4 @@ export type SystemRoute = typeof system
export type EmailConfigRoute = typeof emailConfig
export type AdminInviteCodesRoute = typeof adminInviteCodes
export type PublicInviteCodesRoute = typeof publicInviteCodes
export type AuthProvidersRoute = typeof authProviders
+67 -4
View File
@@ -2,8 +2,15 @@ import crypto from 'node:crypto'
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { admin, organization, username } from 'better-auth/plugins'
import { count, eq } from 'drizzle-orm'
import { genericOAuth } from 'better-auth/plugins/generic-oauth'
import { count, eq, like } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import {
BUILTIN_PROVIDER_IDS,
OAUTH_PROVIDER_KEY_PATTERN,
OAUTH_PROVIDER_KEY_PREFIX,
parseProviderConfig,
} from '../shared/oauth-providers'
import * as authSchema from './db/auth-schema'
import { orgQuotas, systemOptions } from './db/schema'
import type { Database } from './platform/interface'
@@ -30,7 +37,49 @@ async function verifyPassword({ hash, password }: { hash: string; password: stri
return crypto.timingSafeEqual(key, Buffer.from(keyHex, 'hex'))
}
export function createAuth(db: Database, secret: string, baseURL?: string, trustedOrigins?: string[]) {
async function loadProviderConfig(db: Database, providerId: string) {
const rows = await db
.select({ value: systemOptions.value })
.from(systemOptions)
.where(eq(systemOptions.key, `${OAUTH_PROVIDER_KEY_PREFIX}${providerId}`))
const raw = rows[0]?.value
if (!raw) return null
return parseProviderConfig(raw)
}
async function loadOidcConfigs(db: Database) {
const rows = await db
.select({ value: systemOptions.value })
.from(systemOptions)
.where(like(systemOptions.key, OAUTH_PROVIDER_KEY_PATTERN))
const configs = []
for (const r of rows) {
const c = parseProviderConfig(r.value)
if (c && c.type === 'oidc' && c.enabled) configs.push(c)
}
return configs
}
// All 35 built-in providers are registered as async functions so better-auth
// can resolve them on demand. Unconfigured providers return enabled: false
// and are ignored by the framework.
function buildDynamicSocialProviders(db: Database) {
const providers: Record<string, () => Promise<{ clientId: string; clientSecret: string; enabled: boolean }>> = {}
for (const id of BUILTIN_PROVIDER_IDS) {
providers[id] = async () => {
const config = await loadProviderConfig(db, id)
if (!config?.enabled || config.type !== 'builtin') {
return { clientId: '', clientSecret: '', enabled: false }
}
return { clientId: config.clientId, clientSecret: config.clientSecret, enabled: true }
}
}
return providers
}
export async function createAuth(db: Database, secret: string, baseURL?: string, trustedOrigins?: string[]) {
const oidcConfigs = await loadOidcConfigs(db)
return betterAuth({
database: drizzleAdapter(db, { provider: 'sqlite', schema: authSchema }),
secret,
@@ -49,7 +98,21 @@ export function createAuth(db: Database, secret: string, baseURL?: string, trust
maxAge: 60 * 5,
},
},
plugins: [admin(), organization(), username()],
socialProviders: buildDynamicSocialProviders(db),
plugins: [
admin(),
organization(),
username(),
genericOAuth({
config: oidcConfigs.map((c) => ({
providerId: c.providerId,
clientId: c.clientId,
clientSecret: c.clientSecret,
discoveryUrl: c.discoveryUrl,
scopes: c.scopes,
})),
}),
],
databaseHooks: {
user: {
create: {
@@ -86,7 +149,7 @@ export function createAuth(db: Database, secret: string, baseURL?: string, trust
})
}
export type Auth = ReturnType<typeof createAuth>
export type Auth = Awaited<ReturnType<typeof createAuth>>
async function isFirstUser(db: Database): Promise<boolean> {
const [row] = await db.select({ c: count() }).from(authSchema.user)
+1 -1
View File
@@ -11,6 +11,6 @@ const baseURL = process.env.BETTER_AUTH_URL || 'http://localhost:5173'
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || ['http://localhost:5173']
const auth = createAuth(platform.db, secret, baseURL, trustedOrigins)
const auth = await createAuth(platform.db, secret, baseURL, trustedOrigins)
export default createApp(platform, auth)
+8 -8
View File
@@ -2,8 +2,8 @@ import { describe, expect, it } from 'vitest'
import { authedHeaders, createTestApp } from '../test/setup.js'
import { requireAdmin } from './auth.js'
function createAdminTestApp() {
const { app, db, auth } = createTestApp()
async function createAdminTestApp() {
const { app, db, auth } = await createTestApp()
// Add a test-only route protected by requireAdmin
app.get('/api/admin-only', requireAdmin, (c) => c.json({ ok: true }))
return { app, db, auth }
@@ -11,7 +11,7 @@ function createAdminTestApp() {
// Signs up then signs in to get a session cookie that reflects the post-hook role update
async function authedHeadersWithFreshSession(
app: ReturnType<typeof createTestApp>['app'],
app: Awaited<ReturnType<typeof createTestApp>>['app'],
email: string,
password = 'password123456',
name = 'Test User',
@@ -32,13 +32,13 @@ async function authedHeadersWithFreshSession(
describe('requireAdmin middleware', () => {
it('returns 403 when user is not authenticated', async () => {
const { app } = createAdminTestApp()
const { app } = await createAdminTestApp()
const res = await app.request('/api/admin-only')
expect(res.status).toBe(401)
})
it('returns 403 when authenticated user does not have admin role', async () => {
const { app } = createAdminTestApp()
const { app } = await createAdminTestApp()
// First user becomes admin; second does not
await authedHeadersWithFreshSession(app, 'admin@example.com', 'password123456', 'Admin')
const headers = await authedHeaders(app, 'regular@example.com', 'password123456')
@@ -47,7 +47,7 @@ describe('requireAdmin middleware', () => {
})
it('returns Forbidden error body when user lacks admin role', async () => {
const { app } = createAdminTestApp()
const { app } = await createAdminTestApp()
await authedHeadersWithFreshSession(app, 'admin@example.com', 'password123456', 'Admin')
const headers = await authedHeaders(app, 'regular@example.com', 'password123456')
const res = await app.request('/api/admin-only', { headers })
@@ -56,7 +56,7 @@ describe('requireAdmin middleware', () => {
})
it('allows request when user has admin role', async () => {
const { app } = createAdminTestApp()
const { app } = await createAdminTestApp()
// First signup → role updated to admin by hook; sign in to get fresh session
const headers = await authedHeadersWithFreshSession(app, 'admin@example.com', 'password123456', 'Admin')
const res = await app.request('/api/admin-only', { headers })
@@ -64,7 +64,7 @@ describe('requireAdmin middleware', () => {
})
it('returns expected body when admin accesses protected route', async () => {
const { app } = createAdminTestApp()
const { app } = await createAdminTestApp()
const headers = await authedHeadersWithFreshSession(app, 'admin@example.com', 'password123456', 'Admin')
const res = await app.request('/api/admin-only', { headers })
const body = (await res.json()) as { ok: boolean }
+337
View File
@@ -0,0 +1,337 @@
import { describe, expect, it } from 'vitest'
import { adminHeaders, authedHeaders, createTestApp } from '../test/setup.js'
const githubConfig = {
type: 'builtin' as const,
clientId: 'client-id-123',
clientSecret: 'super-secret-value',
enabled: true,
}
const oidcConfig = {
type: 'oidc' as const,
clientId: 'oidc-client-id',
clientSecret: 'oidc-secret-value',
enabled: true,
discoveryUrl: 'https://accounts.example.com/.well-known/openid-configuration',
scopes: ['openid', 'email', 'profile'],
}
async function putProvider(
app: Awaited<ReturnType<typeof createTestApp>>['app'],
headers: Record<string, string>,
providerId: string,
body: Record<string, unknown>,
) {
return app.request(`/api/auth-providers/admin/${providerId}`, {
method: 'PUT',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
describe('Auth Providers — public list', () => {
it('returns empty items when no providers are configured', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/auth-providers')
expect(res.status).toBe(200)
const body = (await res.json()) as { items: unknown[] }
expect(body.items).toEqual([])
})
it('returns only enabled providers', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', { ...githubConfig, enabled: true })
await putProvider(app, admin, 'google', { ...githubConfig, clientId: 'google-id', enabled: false })
const res = await app.request('/api/auth-providers')
expect(res.status).toBe(200)
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items).toHaveLength(1)
expect(body.items[0].providerId).toBe('github')
})
it('does not include clientSecret in public response', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', githubConfig)
const res = await app.request('/api/auth-providers')
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items[0]).not.toHaveProperty('clientSecret')
})
it('returns display name and icon from provider metadata', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', githubConfig)
const res = await app.request('/api/auth-providers')
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items[0].name).toBe('GitHub')
expect(body.items[0].icon).toBe('github')
})
it('uses providerId as fallback name and icon for unknown OIDC provider', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'my-custom-oidc', oidcConfig)
const res = await app.request('/api/auth-providers')
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items).toHaveLength(1)
// No entry in OAuthProviderMeta for 'my-custom-oidc', so falls back to providerId
expect(body.items[0].name).toBe('my-custom-oidc')
expect(body.items[0].icon).toBe('my-custom-oidc')
})
it('disabled provider does not appear in public list', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', { ...githubConfig, enabled: false })
const res = await app.request('/api/auth-providers')
const body = (await res.json()) as { items: unknown[] }
expect(body.items).toHaveLength(0)
})
})
describe('Auth Providers — admin list', () => {
it('returns 401 without authentication', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/auth-providers/admin')
expect(res.status).toBe(401)
})
it('returns 403 for non-admin user', async () => {
const { app } = await createTestApp()
// First sign-up makes admin; second is regular user
await adminHeaders(app)
await authedHeaders(app, 'regular@example.com') // registers the second user
const signInRes = await app.request('/api/auth/sign-in/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'regular@example.com', password: 'password123456' }),
})
const freshHeaders = { Cookie: signInRes.headers.getSetCookie().join('; ') }
const res = await app.request('/api/auth-providers/admin', { headers: freshHeaders })
expect(res.status).toBe(403)
})
it('returns empty items when no providers are configured', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await app.request('/api/auth-providers/admin', { headers: admin })
expect(res.status).toBe(200)
const body = (await res.json()) as { items: unknown[] }
expect(body.items).toEqual([])
})
it('returns all configs including disabled providers', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', { ...githubConfig, enabled: true })
await putProvider(app, admin, 'google', { ...githubConfig, clientId: 'google-id', enabled: false })
const res = await app.request('/api/auth-providers/admin', { headers: admin })
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items).toHaveLength(2)
})
it('masks clientSecret leaving only last 4 chars visible', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', githubConfig)
const res = await app.request('/api/auth-providers/admin', { headers: admin })
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
const secret = body.items[0].clientSecret as string
expect(secret).toMatch(/^\*+alue$/)
expect(secret).not.toBe(githubConfig.clientSecret)
})
it('masks short secret entirely with four asterisks', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', { ...githubConfig, clientSecret: 'abc' })
const res = await app.request('/api/auth-providers/admin', { headers: admin })
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items[0].clientSecret).toBe('****')
})
})
describe('Auth Providers — admin upsert (PUT)', () => {
it('returns 401 without authentication', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/auth-providers/admin/github', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(githubConfig),
})
expect(res.status).toBe(401)
})
it('admin can create a builtin provider', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await putProvider(app, admin, 'github', githubConfig)
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.providerId).toBe('github')
expect(body.type).toBe('builtin')
expect(body.clientId).toBe(githubConfig.clientId)
expect(body.enabled).toBe(true)
})
it('returns masked secret on create response', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await putProvider(app, admin, 'github', githubConfig)
const body = (await res.json()) as Record<string, unknown>
expect(body.clientSecret).not.toBe(githubConfig.clientSecret)
expect((body.clientSecret as string).endsWith('alue')).toBe(true)
})
it('updates an existing provider on second PUT', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', githubConfig)
const res = await putProvider(app, admin, 'github', { ...githubConfig, clientId: 'new-client-id', enabled: false })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.clientId).toBe('new-client-id')
expect(body.enabled).toBe(false)
// Admin list should still have only one entry
const listRes = await app.request('/api/auth-providers/admin', { headers: admin })
const listBody = (await listRes.json()) as { items: unknown[] }
expect(listBody.items).toHaveLength(1)
})
it('admin can create an OIDC provider with discoveryUrl', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await putProvider(app, admin, 'my-oidc', oidcConfig)
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.providerId).toBe('my-oidc')
expect(body.type).toBe('oidc')
expect(body.discoveryUrl).toBe(oidcConfig.discoveryUrl)
expect(body.scopes).toEqual(oidcConfig.scopes)
})
it('returns 400 for unknown builtin provider ID', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await putProvider(app, admin, 'not-a-real-provider', githubConfig)
expect(res.status).toBe(400)
const body = (await res.json()) as Record<string, unknown>
expect(body.error).toMatch(/Unknown builtin provider/)
})
it('returns 400 for OIDC provider missing discoveryUrl', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const { discoveryUrl: _, ...oidcWithoutDiscovery } = oidcConfig
const res = await putProvider(app, admin, 'my-oidc', oidcWithoutDiscovery)
expect(res.status).toBe(400)
const body = (await res.json()) as Record<string, unknown>
expect(body.error).toMatch(/discoveryUrl is required/)
})
it('returns 400 when clientId is missing', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const { clientId: _, ...withoutClientId } = githubConfig
const res = await putProvider(app, admin, 'github', withoutClientId)
expect(res.status).toBe(400)
})
it('returns 400 when clientSecret is empty string', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await putProvider(app, admin, 'github', { ...githubConfig, clientSecret: '' })
expect(res.status).toBe(400)
})
it('returns 400 when type is invalid', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await putProvider(app, admin, 'github', { ...githubConfig, type: 'unknown-type' })
expect(res.status).toBe(400)
})
})
describe('Auth Providers — admin delete', () => {
it('returns 401 without authentication', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/auth-providers/admin/github', { method: 'DELETE' })
expect(res.status).toBe(401)
})
it('admin can delete a provider', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', githubConfig)
const res = await app.request('/api/auth-providers/admin/github', { method: 'DELETE', headers: admin })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.deleted).toBe(true)
expect(body.providerId).toBe('github')
})
it('deleted provider no longer appears in public list', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', githubConfig)
await app.request('/api/auth-providers/admin/github', { method: 'DELETE', headers: admin })
const res = await app.request('/api/auth-providers')
const body = (await res.json()) as { items: unknown[] }
expect(body.items).toHaveLength(0)
})
it('deleted provider no longer appears in admin list', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
await putProvider(app, admin, 'github', githubConfig)
await app.request('/api/auth-providers/admin/github', { method: 'DELETE', headers: admin })
const res = await app.request('/api/auth-providers/admin', { headers: admin })
const body = (await res.json()) as { items: unknown[] }
expect(body.items).toHaveLength(0)
})
it('deleting a non-existent provider returns 200 with deleted flag', async () => {
const { app } = await createTestApp()
const admin = await adminHeaders(app)
const res = await app.request('/api/auth-providers/admin/github', { method: 'DELETE', headers: admin })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.deleted).toBe(true)
})
})
+108
View File
@@ -0,0 +1,108 @@
import { zValidator } from '@hono/zod-validator'
import { eq, like } from 'drizzle-orm'
import { Hono } from 'hono'
import { z } from 'zod'
import {
BUILTIN_PROVIDER_IDS,
isValidProviderId,
OAUTH_PROVIDER_KEY_PATTERN,
OAUTH_PROVIDER_KEY_PREFIX,
OAuthProviderMeta,
parseProviderConfig,
} from '../../shared/oauth-providers'
import { systemOptions } from '../db/schema'
import { requireAdmin } from '../middleware/auth'
import type { Env } from '../middleware/platform'
function optionKey(providerId: string): string {
return `${OAUTH_PROVIDER_KEY_PREFIX}${providerId}`
}
function maskSecret(secret: string): string {
if (secret.length <= 4) return '****'
return `${'*'.repeat(secret.length - 4)}${secret.slice(-4)}`
}
const upsertSchema = z.object({
type: z.enum(['builtin', 'oidc']),
clientId: z.string().min(1),
clientSecret: z.string().min(1),
enabled: z.boolean(),
discoveryUrl: z.string().url().optional(),
scopes: z.array(z.string()).optional(),
})
const app = new Hono<Env>()
// Public: enabled providers only, no secrets (for login page buttons)
.get('/', async (c) => {
const db = c.get('platform').db
const rows = await db.select().from(systemOptions).where(like(systemOptions.key, OAUTH_PROVIDER_KEY_PATTERN))
const items = rows
.map((r) => {
const config = parseProviderConfig(r.value)
if (!config?.enabled) return null
const meta = OAuthProviderMeta[config.providerId]
return {
providerId: config.providerId,
type: config.type,
name: meta?.name ?? config.providerId,
icon: meta?.icon ?? config.providerId,
}
})
.filter((item) => item !== null)
return c.json({ items })
})
// Admin: list all provider configs (secrets masked)
.get('/admin', requireAdmin, async (c) => {
const db = c.get('platform').db
const rows = await db.select().from(systemOptions).where(like(systemOptions.key, OAUTH_PROVIDER_KEY_PATTERN))
const items = rows
.map((r) => {
const config = parseProviderConfig(r.value)
if (!config) return null
return { ...config, clientSecret: maskSecret(config.clientSecret) }
})
.filter((item) => item !== null)
return c.json({ items })
})
// Admin: upsert a provider config
.put('/admin/:providerId', requireAdmin, zValidator('json', upsertSchema), async (c) => {
const db = c.get('platform').db
const providerId = c.req.param('providerId')
const body = c.req.valid('json')
if (!isValidProviderId(providerId)) {
return c.json({ error: 'Provider ID must contain only lowercase letters, numbers, and hyphens' }, 400)
}
if (body.type === 'builtin' && !BUILTIN_PROVIDER_IDS.includes(providerId)) {
return c.json({ error: `Unknown builtin provider: ${providerId}` }, 400)
}
if (body.type === 'oidc' && !body.discoveryUrl) {
return c.json({ error: 'discoveryUrl is required for OIDC providers' }, 400)
}
const config = { providerId, ...body }
const key = optionKey(providerId)
const value = JSON.stringify(config)
const existing = await db.select({ key: systemOptions.key }).from(systemOptions).where(eq(systemOptions.key, key))
if (existing.length > 0) {
await db.update(systemOptions).set({ value, public: false }).where(eq(systemOptions.key, key))
} else {
await db.insert(systemOptions).values({ key, value, public: false })
}
return c.json({ ...config, clientSecret: maskSecret(config.clientSecret) })
})
// Admin: delete a provider config
.delete('/admin/:providerId', requireAdmin, async (c) => {
const db = c.get('platform').db
const providerId = c.req.param('providerId')
if (!isValidProviderId(providerId)) {
return c.json({ error: 'Provider ID must contain only lowercase letters, numbers, and hyphens' }, 400)
}
await db.delete(systemOptions).where(eq(systemOptions.key, optionKey(providerId)))
return c.json({ providerId, deleted: true })
})
export default app
+4 -4
View File
@@ -24,7 +24,7 @@ describe('migration 0004_username_plugin.sql', () => {
describe('username plugin — sign-up with username', () => {
it('sign-up with username stores the username on the user record', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -40,7 +40,7 @@ describe('username plugin — sign-up with username', () => {
})
it('sign-up without username leaves the username column null', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -51,7 +51,7 @@ describe('username plugin — sign-up with username', () => {
})
it('sign-up with duplicate username returns a non-200 response', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -76,7 +76,7 @@ describe('username plugin — sign-up with username', () => {
})
it('two users with different usernames both register successfully', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const res1 = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
+4 -4
View File
@@ -4,15 +4,15 @@ import { createApp } from '../app'
import { createAuth } from '../auth'
import { createCloudflarePlatform } from '../platform/cloudflare'
function buildApp() {
async function buildApp() {
const platform = createCloudflarePlatform(env)
const auth = createAuth(platform.db, env.BETTER_AUTH_SECRET)
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET)
return createApp(platform, auth)
}
describe('[CF] Auth API', () => {
it('POST /api/auth/sign-up/email creates user', async () => {
const app = buildApp()
const app = await buildApp()
const res = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -24,7 +24,7 @@ describe('[CF] Auth API', () => {
})
it('POST /api/auth/sign-in/email signs in', async () => {
const app = buildApp()
const app = await buildApp()
const email = `cf-signin-${Date.now()}@example.com`
await app.request('/api/auth/sign-up/email', {
method: 'POST',
+12 -12
View File
@@ -6,7 +6,7 @@ import { createTestApp } from '../test/setup.js'
describe('Auth API', () => {
it('POST /api/auth/sign-up/email creates user', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -18,7 +18,7 @@ describe('Auth API', () => {
})
it('POST /api/auth/sign-in/email signs in', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
// First sign up
await app.request('/api/auth/sign-up/email', {
method: 'POST',
@@ -36,7 +36,7 @@ describe('Auth API', () => {
})
it('POST /api/auth/sign-in/email rejects wrong password', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -51,7 +51,7 @@ describe('Auth API', () => {
})
it('first user gets admin role after signup', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -62,7 +62,7 @@ describe('Auth API', () => {
})
it('first user gets a personal organization created after signup', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const signUpRes = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -80,7 +80,7 @@ describe('Auth API', () => {
})
it('second user does NOT get admin role', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
// First user
await app.request('/api/auth/sign-up/email', {
method: 'POST',
@@ -98,7 +98,7 @@ describe('Auth API', () => {
})
it('second user also gets a personal organization created', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
// First user
await app.request('/api/auth/sign-up/email', {
method: 'POST',
@@ -123,7 +123,7 @@ describe('Auth API', () => {
})
it('second user gets a member record with owner role in their personal org', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
// First user
await app.request('/api/auth/sign-up/email', {
method: 'POST',
@@ -152,7 +152,7 @@ describe('Auth API', () => {
})
it('signup without default_org_quota set creates an org_quotas row with quota=10485760 (built-in default)', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const signUpRes = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -174,7 +174,7 @@ describe('Auth API', () => {
})
it('signup with default_org_quota set to 1073741824 creates an org_quotas row with quota=1073741824 and used=0', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
await db.insert(schema.systemOptions).values({ key: 'default_org_quota', value: '1073741824' })
const signUpRes = await app.request('/api/auth/sign-up/email', {
method: 'POST',
@@ -197,7 +197,7 @@ describe('Auth API', () => {
})
it('signup with default_org_quota set to 0 does NOT create an org_quotas row', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
await db.insert(schema.systemOptions).values({ key: 'default_org_quota', value: '0' })
await app.request('/api/auth/sign-up/email', {
method: 'POST',
@@ -209,7 +209,7 @@ describe('Auth API', () => {
})
it('sign-in with a malformed stored password hash returns a non-200 error response', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
+21 -21
View File
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as schema from '../db/schema.js'
import { adminHeaders, authedHeaders, createTestApp } from '../test/setup.js'
async function seedSmtpConfig(db: ReturnType<typeof createTestApp>['db']) {
async function seedSmtpConfig(db: Awaited<ReturnType<typeof createTestApp>>['db']) {
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'smtp' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -14,7 +14,7 @@ async function seedSmtpConfig(db: ReturnType<typeof createTestApp>['db']) {
])
}
async function seedHttpConfig(db: ReturnType<typeof createTestApp>['db']) {
async function seedHttpConfig(db: Awaited<ReturnType<typeof createTestApp>>['db']) {
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'http' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -25,13 +25,13 @@ async function seedHttpConfig(db: ReturnType<typeof createTestApp>['db']) {
describe('Admin Email Config API — auth', () => {
it('GET returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/email-config')
expect(res.status).toBe(401)
})
it('GET returns 403 for non-admin user', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
await authedHeaders(app, 'admin@example.com')
await authedHeaders(app, 'regular@example.com')
const signInRes = await app.request('/api/auth/sign-in/email', {
@@ -45,7 +45,7 @@ describe('Admin Email Config API — auth', () => {
})
it('PUT returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/email-config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -55,7 +55,7 @@ describe('Admin Email Config API — auth', () => {
})
it('POST /test returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/email-config/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -67,7 +67,7 @@ describe('Admin Email Config API — auth', () => {
describe('Admin Email Config API — GET', () => {
it('returns { provider: null } when no config exists', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/email-config', { headers })
expect(res.status).toBe(200)
@@ -76,7 +76,7 @@ describe('Admin Email Config API — GET', () => {
})
it('returns masked SMTP config after SMTP config is saved', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await seedSmtpConfig(db)
@@ -97,7 +97,7 @@ describe('Admin Email Config API — GET', () => {
})
it('returns masked HTTP config after HTTP config is saved', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await seedHttpConfig(db)
@@ -111,13 +111,13 @@ describe('Admin Email Config API — GET', () => {
// apiKey must be masked
expect(http.apiKey).not.toBe('my-secret-key')
expect(String(http.apiKey).endsWith('-key')).toBe(true)
expect(String(http.apiKey)).toMatch(/^\*+\-key$/)
expect(String(http.apiKey)).toMatch(/^\*+-key$/)
})
})
describe('Admin Email Config API — PUT', () => {
it('saves SMTP config and returns success', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/email-config', {
@@ -141,7 +141,7 @@ describe('Admin Email Config API — PUT', () => {
})
it('persists SMTP config so GET reflects the saved values', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
await app.request('/api/admin/email-config', {
@@ -170,7 +170,7 @@ describe('Admin Email Config API — PUT', () => {
})
it('saves HTTP config and returns success', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/email-config', {
@@ -191,7 +191,7 @@ describe('Admin Email Config API — PUT', () => {
})
it('persists HTTP config so GET reflects the saved values', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
await app.request('/api/admin/email-config', {
@@ -216,7 +216,7 @@ describe('Admin Email Config API — PUT', () => {
})
it('returns 400 for invalid provider value', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/email-config', {
@@ -228,7 +228,7 @@ describe('Admin Email Config API — PUT', () => {
})
it('returns 400 for invalid from email', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/email-config', {
@@ -240,7 +240,7 @@ describe('Admin Email Config API — PUT', () => {
})
it('updates existing config when PUT is called a second time', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
await app.request('/api/admin/email-config', {
@@ -281,7 +281,7 @@ describe('Admin Email Config API — POST /test', () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
vi.stubGlobal('fetch', fetchMock)
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await seedHttpConfig(db)
@@ -303,7 +303,7 @@ describe('Admin Email Config API — POST /test', () => {
})
vi.stubGlobal('fetch', fetchMock)
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await seedHttpConfig(db)
@@ -319,7 +319,7 @@ describe('Admin Email Config API — POST /test', () => {
})
it('returns 400 when no email config is set', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/email-config/test', {
@@ -334,7 +334,7 @@ describe('Admin Email Config API — POST /test', () => {
})
it('returns 400 for invalid to email', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await seedSmtpConfig(db)
+3 -3
View File
@@ -4,15 +4,15 @@ import { createApp } from '../app'
import { createAuth } from '../auth'
import { createCloudflarePlatform } from '../platform/cloudflare'
function buildApp() {
async function buildApp() {
const platform = createCloudflarePlatform(env)
const auth = createAuth(platform.db, env.BETTER_AUTH_SECRET)
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET)
return createApp(platform, auth)
}
describe('[CF] GET /api/health', () => {
it('returns ok', async () => {
const app = buildApp()
const app = await buildApp()
const res = await app.request('/api/health')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ status: 'ok' })
+1 -1
View File
@@ -3,7 +3,7 @@ import { createTestApp } from '../test/setup.js'
describe('GET /api/health', () => {
it('returns ok', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/health')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ status: 'ok' })
+25 -25
View File
@@ -6,13 +6,13 @@ import { adminHeaders, authedHeaders, createTestApp } from '../test/setup.js'
describe('Admin Invite Codes API — auth guards', () => {
it('GET / returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/invite-codes')
expect(res.status).toBe(401)
})
it('GET / returns 403 for a non-admin user', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
await adminHeaders(app) // first user becomes admin
const headers = await authedHeaders(app, 'regular@example.com')
const res = await app.request('/api/admin/invite-codes', { headers })
@@ -20,7 +20,7 @@ describe('Admin Invite Codes API — auth guards', () => {
})
it('POST / returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/invite-codes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -30,7 +30,7 @@ describe('Admin Invite Codes API — auth guards', () => {
})
it('DELETE /:id returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/invite-codes/someid', { method: 'DELETE' })
expect(res.status).toBe(401)
})
@@ -38,7 +38,7 @@ describe('Admin Invite Codes API — auth guards', () => {
describe('Admin Invite Codes API — GET /', () => {
it('returns an empty list when no codes exist', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/invite-codes', { headers })
expect(res.status).toBe(200)
@@ -47,7 +47,7 @@ describe('Admin Invite Codes API — GET /', () => {
})
it('returns created codes with correct total', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
await app.request('/api/admin/invite-codes', {
@@ -64,7 +64,7 @@ describe('Admin Invite Codes API — GET /', () => {
})
it('paginates with page and pageSize query params', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
await app.request('/api/admin/invite-codes', {
@@ -83,7 +83,7 @@ describe('Admin Invite Codes API — GET /', () => {
describe('Admin Invite Codes API — POST /', () => {
it('creates the requested number of codes and returns 201', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/invite-codes', {
method: 'POST',
@@ -96,7 +96,7 @@ describe('Admin Invite Codes API — POST /', () => {
})
it('creates codes with an expiry when expiresInDays is provided', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/invite-codes', {
method: 'POST',
@@ -109,7 +109,7 @@ describe('Admin Invite Codes API — POST /', () => {
})
it('returns 400 when count is missing', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/invite-codes', {
method: 'POST',
@@ -120,7 +120,7 @@ describe('Admin Invite Codes API — POST /', () => {
})
it('returns 400 when count exceeds maximum of 100', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/invite-codes', {
method: 'POST',
@@ -131,7 +131,7 @@ describe('Admin Invite Codes API — POST /', () => {
})
it('returns 400 when count is zero', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/invite-codes', {
method: 'POST',
@@ -144,7 +144,7 @@ describe('Admin Invite Codes API — POST /', () => {
describe('Admin Invite Codes API — DELETE /:id', () => {
it('deletes an unused code and returns deleted:true', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const [row] = await generateInviteCodes(db, 'admin-user', 1)
@@ -159,7 +159,7 @@ describe('Admin Invite Codes API — DELETE /:id', () => {
})
it('returns 404 for a nonexistent code id', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/invite-codes/nonexistent', {
method: 'DELETE',
@@ -169,7 +169,7 @@ describe('Admin Invite Codes API — DELETE /:id', () => {
})
it('returns 400 when trying to delete an already-used code', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const [row] = await generateInviteCodes(db, 'admin-user', 1)
await redeemInviteCode(db, row.code, 'user-123')
@@ -186,7 +186,7 @@ describe('Admin Invite Codes API — DELETE /:id', () => {
describe('Public Invite Codes API — POST /validate', () => {
it('returns valid:true for a valid unused code', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
const res = await app.request('/api/invite-codes/validate', {
@@ -200,7 +200,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns valid:false for a nonexistent code', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/invite-codes/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -213,7 +213,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns valid:false for a used code', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
await redeemInviteCode(db, row.code, 'user-99')
@@ -228,7 +228,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns valid:false for an expired code', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const past = new Date(Date.now() - 1000)
const [row] = await generateInviteCodes(db, 'admin-1', 1, past)
@@ -243,7 +243,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns 400 when code field is missing from request body', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/invite-codes/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -253,7 +253,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns 400 when code is an empty string', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/invite-codes/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -263,7 +263,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns 400 when code contains lowercase letters', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/invite-codes/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -273,7 +273,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns 400 when code is fewer than 8 characters', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/invite-codes/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -283,7 +283,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('returns 400 when code is more than 8 characters', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/invite-codes/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -293,7 +293,7 @@ describe('Public Invite Codes API — POST /validate', () => {
})
it('is accessible without authentication', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
// No auth headers — should still work
+21 -16
View File
@@ -25,7 +25,7 @@ const validStorage = {
secretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}
async function insertStorage(db: ReturnType<typeof createTestApp>['db'], used = 0) {
async function insertStorage(db: Awaited<ReturnType<typeof createTestApp>>['db'], used = 0) {
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -36,7 +36,7 @@ async function insertStorage(db: ReturnType<typeof createTestApp>['db'], used =
}
async function insertFile(
db: ReturnType<typeof createTestApp>['db'],
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
opts: { id: string; name: string; size?: number; status?: string },
) {
@@ -50,14 +50,19 @@ async function insertFile(
`)
}
async function getOrgId(db: ReturnType<typeof createTestApp>['db']): Promise<string> {
async function getOrgId(db: Awaited<ReturnType<typeof createTestApp>>['db']): Promise<string> {
const rows = await db.all<{ id: string }>(sql`
SELECT id FROM organization WHERE metadata LIKE '%"type":"personal"%' LIMIT 1
`)
return rows[0].id
}
async function setOrgQuota(db: ReturnType<typeof createTestApp>['db'], orgId: string, quota: number, used = 0) {
async function setOrgQuota(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
quota: number,
used = 0,
) {
const existing = await db.select().from(orgQuotas).where(eq(orgQuotas.orgId, orgId))
if (existing.length > 0) {
await db.update(orgQuotas).set({ quota, used }).where(eq(orgQuotas.orgId, orgId))
@@ -70,7 +75,7 @@ async function setOrgQuota(db: ReturnType<typeof createTestApp>['db'], orgId: st
describe('POST /api/objects/:id/copy — quota enforcement', () => {
it('returns 422 when copying a file would exceed quota', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -89,7 +94,7 @@ describe('POST /api/objects/:id/copy — quota enforcement', () => {
})
it('returns 201 and increments orgQuotas.used when copy succeeds within quota', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -109,7 +114,7 @@ describe('POST /api/objects/:id/copy — quota enforcement', () => {
})
it('returns 201 and increments storages.used when copy succeeds', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 50)
const orgId = await getOrgId(db)
@@ -127,7 +132,7 @@ describe('POST /api/objects/:id/copy — quota enforcement', () => {
})
it('returns 201 without incrementing usage when copying a zero-size file', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -152,7 +157,7 @@ describe('POST /api/objects/:id/copy — quota enforcement', () => {
})
it('returns 201 when no quota row exists (unlimited)', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -168,7 +173,7 @@ describe('POST /api/objects/:id/copy — quota enforcement', () => {
})
it('returns 201 when quota is 0 (unlimited) regardless of file size', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -184,7 +189,7 @@ describe('POST /api/objects/:id/copy — quota enforcement', () => {
})
it('returns 404 when source file does not exist', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/copy', {
@@ -200,7 +205,7 @@ describe('POST /api/objects/:id/copy — quota enforcement', () => {
describe('PATCH /api/objects/:id/done — quota enforcement via confirmUpload', () => {
it('returns 200 and increments usage when quota allows', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -217,7 +222,7 @@ describe('PATCH /api/objects/:id/done — quota enforcement via confirmUpload',
})
it('returns 200 and increments storages.used when quota allows', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 100)
const orgId = await getOrgId(db)
@@ -231,7 +236,7 @@ describe('PATCH /api/objects/:id/done — quota enforcement via confirmUpload',
})
it('returns 422 when confirming upload would exceed quota', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -246,7 +251,7 @@ describe('PATCH /api/objects/:id/done — quota enforcement via confirmUpload',
})
it('does not change usage when a file with size 0 is confirmed', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 50)
const orgId = await getOrgId(db)
@@ -262,7 +267,7 @@ describe('PATCH /api/objects/:id/done — quota enforcement via confirmUpload',
})
it('returns 200 when no quota row exists (unlimited)', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
+9 -9
View File
@@ -4,9 +4,9 @@ import { createApp } from '../app'
import { createAuth } from '../auth'
import { createCloudflarePlatform } from '../platform/cloudflare'
function buildApp() {
async function buildApp() {
const platform = createCloudflarePlatform(env)
const auth = createAuth(platform.db, env.BETTER_AUTH_SECRET)
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET)
return createApp(platform, auth)
}
@@ -23,13 +23,13 @@ async function authedHeaders(app: ReturnType<typeof buildApp>) {
describe('[CF] Objects API', () => {
it('returns 401 without auth', async () => {
const app = buildApp()
const app = await buildApp()
const res = await app.request('/api/objects')
expect(res.status).toBe(401)
})
it('GET /api/objects returns empty list', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', { headers })
expect(res.status).toBe(200)
@@ -38,7 +38,7 @@ describe('[CF] Objects API', () => {
})
it('POST /api/objects returns 400 for invalid input', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', {
method: 'POST',
@@ -49,7 +49,7 @@ describe('[CF] Objects API', () => {
})
it('POST /api/objects returns 500 when no storage configured', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', {
method: 'POST',
@@ -60,14 +60,14 @@ describe('[CF] Objects API', () => {
})
it('GET /api/objects/:id returns 404 for missing object', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', { headers })
expect(res.status).toBe(404)
})
it('PATCH /api/objects/:id returns 404 for missing object', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', {
method: 'PATCH',
@@ -78,7 +78,7 @@ describe('[CF] Objects API', () => {
})
it('DELETE /api/objects/:id returns 404 for missing object', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', {
method: 'DELETE',
+73 -73
View File
@@ -36,7 +36,7 @@ const validStorage = {
secretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}
async function insertStorage(db: ReturnType<typeof createTestApp>['db']) {
async function insertStorage(db: Awaited<ReturnType<typeof createTestApp>>['db']) {
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -45,7 +45,7 @@ async function insertStorage(db: ReturnType<typeof createTestApp>['db']) {
}
async function insertFolder(
db: ReturnType<typeof createTestApp>['db'],
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
opts: { id: string; name: string; parent?: string },
) {
@@ -57,7 +57,7 @@ async function insertFolder(
}
async function insertFile(
db: ReturnType<typeof createTestApp>['db'],
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
opts: { id: string; name: string; parent?: string; status?: string },
) {
@@ -69,7 +69,7 @@ async function insertFile(
`)
}
async function getOrgId(db: ReturnType<typeof createTestApp>['db']): Promise<string> {
async function getOrgId(db: Awaited<ReturnType<typeof createTestApp>>['db']): Promise<string> {
const rows = await db.all<{ id: string }>(sql`
SELECT id FROM organization WHERE metadata LIKE '%"type":"personal"%' LIMIT 1
`)
@@ -78,13 +78,13 @@ async function getOrgId(db: ReturnType<typeof createTestApp>['db']): Promise<str
describe('Objects API', () => {
it('returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/objects')
expect(res.status).toBe(401)
})
it('GET /api/objects returns empty list', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', { headers })
expect(res.status).toBe(200)
@@ -93,7 +93,7 @@ describe('Objects API', () => {
})
it('GET /api/objects respects pagination params', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects?page=2&pageSize=10', { headers })
expect(res.status).toBe(200)
@@ -103,7 +103,7 @@ describe('Objects API', () => {
})
it('POST /api/objects creates a folder', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const res = await app.request('/api/objects', {
@@ -121,7 +121,7 @@ describe('Objects API', () => {
})
it('POST /api/objects returns 400 for invalid input', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', {
method: 'POST',
@@ -132,7 +132,7 @@ describe('Objects API', () => {
})
it('POST /api/objects returns 500 when no storage available', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', {
method: 'POST',
@@ -143,7 +143,7 @@ describe('Objects API', () => {
})
it('GET /api/objects lists active objects in root', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -162,7 +162,7 @@ describe('Objects API', () => {
})
it('GET /api/objects filters by parent', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -178,7 +178,7 @@ describe('Objects API', () => {
})
it('GET /api/objects filters by status', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -193,7 +193,7 @@ describe('Objects API', () => {
})
it('GET /api/objects/:id returns folder detail', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -209,14 +209,14 @@ describe('Objects API', () => {
})
it('GET /api/objects/:id returns 404 for missing object', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', { headers })
expect(res.status).toBe(404)
})
it('PATCH /api/objects/:id renames an object', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -233,7 +233,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id moves an object', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -251,7 +251,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id returns 404 for missing object', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', {
method: 'PATCH',
@@ -262,7 +262,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/done confirms upload', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -278,7 +278,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/done returns 404 for non-draft object', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -292,7 +292,7 @@ describe('Objects API', () => {
})
it('DELETE /api/objects/:id rejects active object (must trash first)', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -303,7 +303,7 @@ describe('Objects API', () => {
})
it('DELETE /api/objects/:id permanently deletes a trashed folder', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -323,7 +323,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/trash trashes a file', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -341,7 +341,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/restore restores a trashed file', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -354,7 +354,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/trash cascades to folder children', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -379,7 +379,7 @@ describe('Objects API', () => {
})
it('POST /api/recycle-bin/empty purges all trashed items', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -396,7 +396,7 @@ describe('Objects API', () => {
})
it('DELETE /api/objects/:id returns 404 for missing object', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', {
method: 'DELETE',
@@ -406,7 +406,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/:id/copy copies a folder', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -425,7 +425,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/:id/copy returns 404 for missing source', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/copy', {
method: 'POST',
@@ -436,7 +436,7 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/done returns 404 for missing object', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/done', {
method: 'PATCH',
@@ -446,7 +446,7 @@ describe('Objects API', () => {
})
it('POST /api/objects creates a file with upload URL', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const res = await app.request('/api/objects', {
@@ -462,7 +462,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/:id/copy copies a file with S3', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -478,7 +478,7 @@ describe('Objects API', () => {
})
it('DELETE /api/objects/:id permanently deletes a trashed file with S3 cleanup', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -493,7 +493,7 @@ describe('Objects API', () => {
})
it('DELETE /api/objects/:id purges folder with file children from S3', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -510,14 +510,14 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/trash returns 404 for missing object', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/trash', { method: 'PATCH', headers })
expect(res.status).toBe(404)
})
it('PATCH /api/objects/:id/trash is idempotent for already-trashed item', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -529,14 +529,14 @@ describe('Objects API', () => {
})
it('PATCH /api/objects/:id/restore returns 404 for missing object', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/restore', { method: 'PATCH', headers })
expect(res.status).toBe(404)
})
it('PATCH /api/objects/:id/restore is no-op for active item', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -549,7 +549,7 @@ describe('Objects API', () => {
})
it('POST /api/recycle-bin/empty with files calls S3 deleteObjects', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -561,7 +561,7 @@ describe('Objects API', () => {
})
it('POST /api/recycle-bin/empty handles folders (no S3 object) and files together', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -578,7 +578,7 @@ describe('Objects API', () => {
})
it('POST /api/recycle-bin/empty returns 0 when trash is empty', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/recycle-bin/empty', { method: 'POST', headers })
expect(res.status).toBe(200)
@@ -587,7 +587,7 @@ describe('Objects API', () => {
})
it('GET /api/objects/:id returns downloadUrl for files', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -600,7 +600,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/move moves multiple items', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -618,7 +618,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/move returns 400 for invalid input', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/batch/move', {
method: 'POST',
@@ -629,7 +629,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/move returns 400 if any id missing from org', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -643,7 +643,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/trash trashes items and cascades', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -661,7 +661,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/trash returns 400 for invalid input', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/batch/trash', {
method: 'POST',
@@ -672,7 +672,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/delete permanently deletes trashed items', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -695,7 +695,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/delete returns 400 if any item is not trashed', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -711,7 +711,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/delete returns 400 for invalid input', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/batch/delete', {
method: 'POST',
@@ -722,7 +722,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/delete decrements usage for files with size > 0', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -750,7 +750,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/trash returns 400 when IDs do not belong to org', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -767,7 +767,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/move returns 400 when moving folder into itself', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -784,7 +784,7 @@ describe('Objects API', () => {
})
it('POST /api/objects/batch/move cascades path when moving a folder', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
@@ -805,7 +805,7 @@ describe('Objects API', () => {
describe('Matter service', () => {
it('createMatter applies defaults for optional fields', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -828,7 +828,7 @@ describe('Matter service', () => {
})
it('createMatter uses provided optional fields', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -852,7 +852,7 @@ describe('Matter service', () => {
})
it('listMatters returns paginated results', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -885,25 +885,25 @@ describe('Matter service', () => {
})
it('getMatter returns null for missing record', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await getMatter(db, 'nonexistent', 'org-1')
expect(result).toBeNull()
})
it('updateMatter returns null for missing record', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await updateMatter(db, 'nonexistent', 'org-1', { name: 'new' })
expect(result).toBeNull()
})
it('confirmUpload returns null for missing record', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const { matter } = await confirmUpload(db, 'nonexistent', 'org-1')
expect(matter).toBeNull()
})
it('confirmUpload returns null for non-draft status', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -922,7 +922,7 @@ describe('Matter service', () => {
})
it('deleteMatter removes and returns the record', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -946,13 +946,13 @@ describe('Matter service', () => {
})
it('deleteMatter returns null for missing record', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await deleteMatter(db, 'nonexistent', 'org-1')
expect(result).toBeNull()
})
it('copyMatter creates a new record from source', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -979,13 +979,13 @@ describe('Matter service', () => {
})
it('getMatters returns empty array for empty ids list', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await getMatters(db, 'org-1', [])
expect(result).toEqual([])
})
it('batchMove moves multiple items to a new parent', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -1017,7 +1017,7 @@ describe('Matter service', () => {
})
it('batchMove throws if any ID does not belong to the org', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -1038,7 +1038,7 @@ describe('Matter service', () => {
})
it('batchTrash sets status to trashed for multiple items', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -1068,7 +1068,7 @@ describe('Matter service', () => {
})
it('batchTrash cascades into folder children', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -1103,7 +1103,7 @@ describe('Matter service', () => {
})
it('batchTrash throws if any ID does not belong to the org', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -1124,7 +1124,7 @@ describe('Matter service', () => {
})
it('batchDelete permanently deletes trashed items', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -1155,7 +1155,7 @@ describe('Matter service', () => {
})
it('batchDelete throws if any item is not trashed', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
@@ -1184,7 +1184,7 @@ describe('Matter service', () => {
})
it('batchDelete throws if any ID does not belong to the org', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, title, mode, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
+11 -11
View File
@@ -16,13 +16,13 @@ async function adminHeaders(app: ReturnType<typeof import('../app')['createApp']
describe('Admin Quotas API', () => {
it('returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/quotas')
expect(res.status).toBe(401)
})
it('returns 403 for non-admin', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
await authedHeaders(app, 'admin@example.com')
await authedHeaders(app, 'regular@example.com')
const signInRes = await app.request('/api/auth/sign-in/email', {
@@ -36,7 +36,7 @@ describe('Admin Quotas API', () => {
})
it('GET /api/admin/quotas returns the default quota row created at signup', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/quotas', { headers })
expect(res.status).toBe(200)
@@ -47,7 +47,7 @@ describe('Admin Quotas API', () => {
})
it('PUT /api/admin/quotas/:orgId creates quota for org', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
// Find the admin's personal org
@@ -72,7 +72,7 @@ describe('Admin Quotas API', () => {
})
it('PUT /api/admin/quotas/:orgId updates existing quota', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const orgs = await db.all<{ id: string }>(
@@ -99,7 +99,7 @@ describe('Admin Quotas API', () => {
})
it('PUT /api/admin/quotas/:orgId rejects negative quota', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/quotas/some-org', {
method: 'PUT',
@@ -110,7 +110,7 @@ describe('Admin Quotas API', () => {
})
it('GET /api/admin/quotas lists quotas with org info', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const orgs = await db.all<{ id: string }>(
@@ -138,13 +138,13 @@ describe('Admin Quotas API', () => {
describe('User Quotas API — /api/quotas', () => {
it('GET /api/quotas/me returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/quotas/me')
expect(res.status).toBe(401)
})
it('GET /api/quotas/me returns the built-in default quota of 10MB when no system option is set', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/quotas/me', { headers })
expect(res.status).toBe(200)
@@ -155,7 +155,7 @@ describe('User Quotas API — /api/quotas', () => {
})
it('GET /api/quotas/me returns 404 when user has no org', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const _headers = await authedHeaders(app, 'noorg@example.com')
// Delete the user's org membership and org to simulate no org
const users = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'noorg@example.com'`)
@@ -172,7 +172,7 @@ describe('User Quotas API — /api/quotas', () => {
})
it('GET /api/quotas/me returns quota after admin sets it', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const adminH = await adminHeaders(app)
// Find admin's org
+8 -8
View File
@@ -6,9 +6,9 @@ import { createAuth } from '../auth'
import { user } from '../db/auth-schema'
import { createCloudflarePlatform } from '../platform/cloudflare'
function buildApp() {
async function buildApp() {
const platform = createCloudflarePlatform(env)
const auth = createAuth(platform.db, env.BETTER_AUTH_SECRET)
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET)
return createApp(platform, auth)
}
@@ -43,13 +43,13 @@ const validStorage = {
describe('[CF] Admin Storages API', () => {
it('returns 401 without auth', async () => {
const app = buildApp()
const app = await buildApp()
const res = await app.request('/api/admin/storages')
expect(res.status).toBe(401)
})
it('GET /api/admin/storages returns empty list', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/storages', { headers })
expect(res.status).toBe(200)
@@ -58,7 +58,7 @@ describe('[CF] Admin Storages API', () => {
})
it('POST /api/admin/storages creates a storage', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/storages', {
method: 'POST',
@@ -73,7 +73,7 @@ describe('[CF] Admin Storages API', () => {
})
it('GET /api/admin/storages/:id returns storage detail', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await adminHeaders(app)
const createRes = await app.request('/api/admin/storages', {
@@ -90,7 +90,7 @@ describe('[CF] Admin Storages API', () => {
})
it('PUT /api/admin/storages/:id updates a storage', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await adminHeaders(app)
const createRes = await app.request('/api/admin/storages', {
@@ -111,7 +111,7 @@ describe('[CF] Admin Storages API', () => {
})
it('DELETE /api/admin/storages/:id deletes a storage', async () => {
const app = buildApp()
const app = await buildApp()
const headers = await adminHeaders(app)
const createRes = await app.request('/api/admin/storages', {
+24 -24
View File
@@ -15,13 +15,13 @@ const validStorage = {
describe('Admin Storages API', () => {
it('returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/storages')
expect(res.status).toBe(401)
})
it('returns 403 for non-admin user', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
// First user becomes admin
await authedHeaders(app, 'admin@example.com')
// Second user is non-admin
@@ -37,7 +37,7 @@ describe('Admin Storages API', () => {
})
it('GET / returns empty list', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/storages', { headers })
expect(res.status).toBe(200)
@@ -46,7 +46,7 @@ describe('Admin Storages API', () => {
})
it('POST / creates a storage', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/storages', {
method: 'POST',
@@ -65,7 +65,7 @@ describe('Admin Storages API', () => {
})
it('GET / lists created storages', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
await app.request('/api/admin/storages', {
@@ -83,7 +83,7 @@ describe('Admin Storages API', () => {
})
it('GET /:id returns storage detail', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const createRes = await app.request('/api/admin/storages', {
@@ -101,14 +101,14 @@ describe('Admin Storages API', () => {
})
it('GET /:id returns 404 for missing storage', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/storages/nonexistent', { headers })
expect(res.status).toBe(404)
})
it('PUT /:id updates a storage', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const createRes = await app.request('/api/admin/storages', {
@@ -130,7 +130,7 @@ describe('Admin Storages API', () => {
})
it('PUT /:id returns 404 for missing storage', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/storages/nonexistent', {
method: 'PUT',
@@ -141,7 +141,7 @@ describe('Admin Storages API', () => {
})
it('DELETE /:id deletes a storage', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const createRes = await app.request('/api/admin/storages', {
@@ -161,7 +161,7 @@ describe('Admin Storages API', () => {
})
it('DELETE /:id returns 404 for missing storage', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/storages/nonexistent', {
method: 'DELETE',
@@ -171,7 +171,7 @@ describe('Admin Storages API', () => {
})
it('DELETE /:id returns 409 when matters reference the storage', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const createRes = await app.request('/api/admin/storages', {
@@ -197,7 +197,7 @@ describe('Admin Storages API', () => {
// Helper to insert a storage row directly into the DB for service-level tests
async function insertStorage(
db: ReturnType<typeof createTestApp>['db'],
db: Awaited<ReturnType<typeof createTestApp>>['db'],
opts: {
id: string
mode: 'private' | 'public'
@@ -220,7 +220,7 @@ async function insertStorage(
describe('selectStorage service', () => {
it('returns the single active storage when capacity is unlimited (0)', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', capacity: 0, used: 0 })
const storage = await selectStorage(db, 'private')
@@ -228,7 +228,7 @@ describe('selectStorage service', () => {
})
it('returns storage when used is below capacity', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', capacity: 100, used: 50 })
const storage = await selectStorage(db, 'private')
@@ -236,7 +236,7 @@ describe('selectStorage service', () => {
})
it('skips storage where used equals capacity', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', capacity: 100, used: 100, createdAt: 1 })
await insertStorage(db, { id: 's2', mode: 'private', capacity: 200, used: 50, createdAt: 2 })
@@ -245,7 +245,7 @@ describe('selectStorage service', () => {
})
it('skips storage where used exceeds capacity', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', capacity: 100, used: 110, createdAt: 1 })
await insertStorage(db, { id: 's2', mode: 'private', capacity: 0, used: 0, createdAt: 2 })
@@ -254,7 +254,7 @@ describe('selectStorage service', () => {
})
it('picks the oldest active storage first (sequential fill order)', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', capacity: 0, used: 0, createdAt: 1 })
await insertStorage(db, { id: 's2', mode: 'private', capacity: 0, used: 0, createdAt: 2 })
@@ -263,7 +263,7 @@ describe('selectStorage service', () => {
})
it('ignores disabled storages', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', status: 'disabled', capacity: 0, createdAt: 1 })
await insertStorage(db, { id: 's2', mode: 'private', status: 'active', capacity: 0, createdAt: 2 })
@@ -272,20 +272,20 @@ describe('selectStorage service', () => {
})
it('ignores storages of a different mode', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'public', capacity: 0 })
await expect(selectStorage(db, 'private')).rejects.toThrow('No available storage')
})
it('throws when no active storage exists for the requested mode', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await expect(selectStorage(db, 'private')).rejects.toThrow('No available storage')
})
it('throws when all storages of the mode are at full capacity', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', capacity: 50, used: 50 })
await insertStorage(db, { id: 's2', mode: 'private', capacity: 100, used: 100 })
@@ -293,7 +293,7 @@ describe('selectStorage service', () => {
})
it('returns a public storage when mode is public', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'public', capacity: 0 })
const storage = await selectStorage(db, 'public')
@@ -301,7 +301,7 @@ describe('selectStorage service', () => {
})
it('does not return a public storage when private mode is requested', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await insertStorage(db, { id: 's1', mode: 'private', capacity: 0, createdAt: 1 })
await insertStorage(db, { id: 's2', mode: 'public', capacity: 0, createdAt: 2 })
+4 -4
View File
@@ -4,15 +4,15 @@ import { createApp } from '../app'
import { createAuth } from '../auth'
import { createCloudflarePlatform } from '../platform/cloudflare'
function buildApp() {
async function buildApp() {
const platform = createCloudflarePlatform(env)
const auth = createAuth(platform.db, env.BETTER_AUTH_SECRET)
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET)
return createApp(platform, auth)
}
describe('[CF] System API', () => {
it('GET /api/system/options returns empty list without auth', async () => {
const app = buildApp()
const app = await buildApp()
const res = await app.request('/api/system/options')
expect(res.status).toBe(200)
const body = (await res.json()) as { items: unknown[]; total: number }
@@ -20,7 +20,7 @@ describe('[CF] System API', () => {
})
it('GET unknown option returns 404', async () => {
const app = buildApp()
const app = await buildApp()
const res = await app.request('/api/system/options/nonexistent_key')
expect(res.status).toBe(404)
})
+4 -4
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import { adminHeaders, createTestApp } from '../test/setup.js'
async function putOption(
app: ReturnType<typeof createTestApp>['app'],
app: Awaited<ReturnType<typeof createTestApp>>['app'],
headers: Record<string, string>,
key: string,
body: Record<string, unknown>,
@@ -16,13 +16,13 @@ async function putOption(
describe('System API — options CRUD', () => {
it('GET unknown key returns 404', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/system/options/site_name')
expect(res.status).toBe(404)
})
it('full admin CRUD lifecycle with public/private visibility', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const admin = await adminHeaders(app)
// Create public option
@@ -72,7 +72,7 @@ describe('System API — options CRUD', () => {
})
it('unauthenticated mutations are rejected', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/system/options/site_name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
+9 -9
View File
@@ -25,13 +25,13 @@ async function signUpUser(app: ReturnType<typeof import('../app')['createApp']>,
describe('Admin Users API', () => {
it('returns 401 without auth', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const res = await app.request('/api/admin/users')
expect(res.status).toBe(401)
})
it('returns 403 for non-admin user', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
// Create first user (auto-admin), then second user (non-admin)
await authedHeaders(app, 'admin@example.com')
const _headers = await authedHeaders(app, 'regular@example.com')
@@ -47,7 +47,7 @@ describe('Admin Users API', () => {
})
it('GET /api/admin/users lists users with pagination', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/users', { headers })
@@ -60,7 +60,7 @@ describe('Admin Users API', () => {
})
it('PUT /api/admin/users/:id/status disables a user', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
// Create a second user
@@ -85,7 +85,7 @@ describe('Admin Users API', () => {
})
it('PUT /api/admin/users/:id/status rejects invalid status', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/users/someid/status', {
method: 'PUT',
@@ -96,7 +96,7 @@ describe('Admin Users API', () => {
})
it('PUT /api/admin/users/:id/status returns 404 for missing user', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/users/nonexistent/status', {
method: 'PUT',
@@ -107,7 +107,7 @@ describe('Admin Users API', () => {
})
it('DELETE /api/admin/users/:id deletes a user', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'todelete@example.com')
@@ -128,7 +128,7 @@ describe('Admin Users API', () => {
})
it('disabled user is rejected by auth middleware on existing session', async () => {
const { app, db } = createTestApp()
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
// Create a second user and get their session before banning
@@ -152,7 +152,7 @@ describe('Admin Users API', () => {
})
it('DELETE /api/admin/users/:id returns 404 for missing user', async () => {
const { app } = createTestApp()
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/admin/users/nonexistent', {
method: 'DELETE',
+13 -13
View File
@@ -11,18 +11,18 @@ vi.mock('nodemailer', () => ({
describe('getEmailConfig', () => {
it('throws when email_provider is not set', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await expect(getEmailConfig(db)).rejects.toThrow('Email provider not configured')
})
it('throws when email_from is not set', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values({ key: 'email_provider', value: 'smtp' })
await expect(getEmailConfig(db)).rejects.toThrow('Email sender not configured')
})
it('throws when SMTP host is missing', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'smtp' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -31,7 +31,7 @@ describe('getEmailConfig', () => {
})
it('throws when SMTP port is missing', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'smtp' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -41,7 +41,7 @@ describe('getEmailConfig', () => {
})
it('returns SMTP config when provider is smtp and all required options are set', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'smtp' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -63,7 +63,7 @@ describe('getEmailConfig', () => {
})
it('returns SMTP config with secure=false when email_smtp_secure is not "true"', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'smtp' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -78,7 +78,7 @@ describe('getEmailConfig', () => {
})
it('throws when HTTP url is missing', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'http' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -87,7 +87,7 @@ describe('getEmailConfig', () => {
})
it('throws when HTTP apiKey is missing', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'http' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -97,7 +97,7 @@ describe('getEmailConfig', () => {
})
it('returns HTTP config when provider is http and all required options are set', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'http' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -113,7 +113,7 @@ describe('getEmailConfig', () => {
})
it('throws when provider is an unknown value', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'unknown' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -129,7 +129,7 @@ describe('sendEmail — SMTP provider', () => {
it('calls nodemailer sendMail with correct parameters', async () => {
sendMailMock.mockResolvedValue({})
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'smtp' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -156,7 +156,7 @@ describe('sendEmail — HTTP provider', () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
vi.stubGlobal('fetch', fetchMock)
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'http' },
{ key: 'email_from', value: 'no-reply@example.com' },
@@ -189,7 +189,7 @@ describe('sendEmail — HTTP provider', () => {
})
vi.stubGlobal('fetch', fetchMock)
const { db } = createTestApp()
const { db } = await createTestApp()
await db.insert(schema.systemOptions).values([
{ key: 'email_provider', value: 'http' },
{ key: 'email_from', value: 'no-reply@example.com' },
+30 -30
View File
@@ -10,26 +10,26 @@ import {
describe('generateInviteCodes', () => {
it('returns the requested number of codes', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-1', 5)
expect(codes).toHaveLength(5)
})
it('returns one code when count is 1', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-1', 1)
expect(codes).toHaveLength(1)
})
it('generates unique codes for each entry', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-1', 10)
const uniqueCodes = new Set(codes.map((c) => c.code))
expect(uniqueCodes.size).toBe(10)
})
it('each code has an 8-character uppercase alphanumeric code field', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-1', 3)
for (const code of codes) {
expect(code.code).toMatch(/^[0-9A-Z]{8}$/)
@@ -37,7 +37,7 @@ describe('generateInviteCodes', () => {
})
it('sets createdBy to the provided admin user id on all codes', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-42', 3)
for (const code of codes) {
expect(code.createdBy).toBe('admin-42')
@@ -45,7 +45,7 @@ describe('generateInviteCodes', () => {
})
it('sets usedBy and usedAt to null on fresh codes', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-1', 2)
for (const code of codes) {
expect(code.usedBy).toBeNull()
@@ -54,7 +54,7 @@ describe('generateInviteCodes', () => {
})
it('sets expiresAt to null when not provided', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-1', 2)
for (const code of codes) {
expect(code.expiresAt).toBeNull()
@@ -62,7 +62,7 @@ describe('generateInviteCodes', () => {
})
it('propagates expiresAt to all generated codes', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const expiry = new Date(Date.now() + 86400000)
const codes = await generateInviteCodes(db, 'admin-1', 3, expiry)
for (const code of codes) {
@@ -71,7 +71,7 @@ describe('generateInviteCodes', () => {
})
it('persists codes to the database', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const codes = await generateInviteCodes(db, 'admin-1', 2)
for (const code of codes) {
const result = await validateInviteCode(db, code.code)
@@ -82,21 +82,21 @@ describe('generateInviteCodes', () => {
describe('validateInviteCode', () => {
it('returns valid:true for an unused, unexpired code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
const result = await validateInviteCode(db, row.code)
expect(result).toEqual({ valid: true })
})
it('returns valid:false with an error for a nonexistent code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await validateInviteCode(db, 'NOSUCHCD')
expect(result.valid).toBe(false)
expect(result.error).toBeTruthy()
})
it('returns valid:false with an error for a used code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
await redeemInviteCode(db, row.code, 'user-99')
const result = await validateInviteCode(db, row.code)
@@ -105,7 +105,7 @@ describe('validateInviteCode', () => {
})
it('returns valid:false with an error for an expired code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const pastDate = new Date(Date.now() - 1000)
const [row] = await generateInviteCodes(db, 'admin-1', 1, pastDate)
const result = await validateInviteCode(db, row.code)
@@ -114,7 +114,7 @@ describe('validateInviteCode', () => {
})
it('returns valid:true for a code that has not yet expired', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const futureDate = new Date(Date.now() + 86400000)
const [row] = await generateInviteCodes(db, 'admin-1', 1, futureDate)
const result = await validateInviteCode(db, row.code)
@@ -124,14 +124,14 @@ describe('validateInviteCode', () => {
describe('redeemInviteCode', () => {
it('returns ok when redeeming a valid unused code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
const result = await redeemInviteCode(db, row.code, 'user-55')
expect(result).toBe('ok')
})
it('marks the code as used so it cannot be validated again', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
await redeemInviteCode(db, row.code, 'user-55')
const check = await validateInviteCode(db, row.code)
@@ -139,13 +139,13 @@ describe('redeemInviteCode', () => {
})
it('returns not_found for a nonexistent code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await redeemInviteCode(db, 'NOSUCHCD', 'user-55')
expect(result).toBe('not_found')
})
it('returns already_used when redeeming a previously redeemed code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
await redeemInviteCode(db, row.code, 'user-55')
const result = await redeemInviteCode(db, row.code, 'user-99')
@@ -153,7 +153,7 @@ describe('redeemInviteCode', () => {
})
it('returns expired for an expired code', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const pastDate = new Date(Date.now() - 1000)
const [row] = await generateInviteCodes(db, 'admin-1', 1, pastDate)
const result = await redeemInviteCode(db, row.code, 'user-55')
@@ -161,7 +161,7 @@ describe('redeemInviteCode', () => {
})
it('sets usedAt to a non-null timestamp after redemption', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
await redeemInviteCode(db, row.code, 'user-55')
const check = await validateInviteCode(db, row.code)
@@ -171,13 +171,13 @@ describe('redeemInviteCode', () => {
describe('listInviteCodes', () => {
it('returns empty items and total 0 when no codes exist', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await listInviteCodes(db, 1, 20)
expect(result).toEqual({ items: [], total: 0 })
})
it('returns all codes when fewer than pageSize', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await generateInviteCodes(db, 'admin-1', 3)
const result = await listInviteCodes(db, 1, 20)
expect(result.total).toBe(3)
@@ -185,7 +185,7 @@ describe('listInviteCodes', () => {
})
it('paginates correctly — page 1 returns first pageSize items', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await generateInviteCodes(db, 'admin-1', 5)
const result = await listInviteCodes(db, 1, 3)
expect(result.total).toBe(5)
@@ -193,7 +193,7 @@ describe('listInviteCodes', () => {
})
it('paginates correctly — page 2 returns remaining items', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await generateInviteCodes(db, 'admin-1', 5)
const result = await listInviteCodes(db, 2, 3)
expect(result.total).toBe(5)
@@ -201,7 +201,7 @@ describe('listInviteCodes', () => {
})
it('returns empty items on a page beyond total count', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await generateInviteCodes(db, 'admin-1', 2)
const result = await listInviteCodes(db, 5, 20)
expect(result.total).toBe(2)
@@ -209,7 +209,7 @@ describe('listInviteCodes', () => {
})
it('orders results by createdAt descending', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await generateInviteCodes(db, 'admin-1', 3)
const result = await listInviteCodes(db, 1, 20)
const timestamps = result.items.map((item) => item.createdAt.getTime())
@@ -220,14 +220,14 @@ describe('listInviteCodes', () => {
describe('deleteInviteCode', () => {
it('returns ok and removes the code when it exists and is unused', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
const result = await deleteInviteCode(db, row.id)
expect(result).toBe('ok')
})
it('removes the code from the database after deletion', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
await deleteInviteCode(db, row.id)
const check = await validateInviteCode(db, row.code)
@@ -235,13 +235,13 @@ describe('deleteInviteCode', () => {
})
it('returns not_found for a nonexistent code id', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await deleteInviteCode(db, 'NOSUCHID')
expect(result).toBe('not_found')
})
it('returns already_used for a code that has been redeemed', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const [row] = await generateInviteCodes(db, 'admin-1', 1)
await redeemInviteCode(db, row.code, 'user-99')
const result = await deleteInviteCode(db, row.id)
+23 -23
View File
@@ -5,7 +5,7 @@ import { orgQuotas } from '../db/schema.js'
import { createTestApp } from '../test/setup.js'
import { confirmUpload, incrementUsageIfAllowed, listTrashedRoots, updateMatter } from './matter.js'
type TestDb = ReturnType<typeof createTestApp>['db']
type TestDb = Awaited<ReturnType<typeof createTestApp>>['db']
async function insertStorage(db: TestDb, opts: { id?: string; used?: number } = {}) {
const id = opts.id ?? 'st-1'
@@ -42,7 +42,7 @@ async function insertDraftFile(
describe('incrementUsageIfAllowed', () => {
it('returns true and increments when no quota row exists (unlimited)', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-ul', used: 0 })
@@ -54,7 +54,7 @@ describe('incrementUsageIfAllowed', () => {
})
it('returns true and increments when quota is 0 (unlimited)', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-q0', used: 100 })
await insertOrgQuota(db, orgId, 0, 5000)
@@ -67,7 +67,7 @@ describe('incrementUsageIfAllowed', () => {
})
it('returns true and increments when used + bytes is within quota', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-in', used: 0 })
await insertOrgQuota(db, orgId, 1000, 400)
@@ -78,7 +78,7 @@ describe('incrementUsageIfAllowed', () => {
})
it('returns true and increments when used + bytes is exactly at quota', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-exact', used: 0 })
await insertOrgQuota(db, orgId, 1000, 500)
@@ -91,7 +91,7 @@ describe('incrementUsageIfAllowed', () => {
})
it('returns false and does not increment when used + bytes exceeds quota', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-over', used: 50 })
await insertOrgQuota(db, orgId, 1000, 800)
@@ -106,7 +106,7 @@ describe('incrementUsageIfAllowed', () => {
})
it('returns false and does not increment when quota is fully consumed', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-full', used: 100 })
await insertOrgQuota(db, orgId, 1000, 1000)
@@ -117,7 +117,7 @@ describe('incrementUsageIfAllowed', () => {
})
it('increments orgQuotas.used when within quota', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-q-inc', used: 0 })
await insertOrgQuota(db, orgId, 5000, 200)
@@ -129,7 +129,7 @@ describe('incrementUsageIfAllowed', () => {
})
it('increments storages.used when within quota', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-s-inc', used: 100 })
await insertOrgQuota(db, orgId, 5000, 100)
@@ -145,7 +145,7 @@ describe('incrementUsageIfAllowed', () => {
describe('confirmUpload', () => {
it('returns { matter } with status active and increments usage for a draft file with size > 0', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-conf', used: 0 })
await insertOrgQuota(db, orgId, 10000, 0)
@@ -164,7 +164,7 @@ describe('confirmUpload', () => {
})
it('returns { matter } and does not increment usage when file size is 0', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-conf2', used: 50 })
await insertOrgQuota(db, orgId, 10000, 50)
@@ -180,14 +180,14 @@ describe('confirmUpload', () => {
})
it('returns { matter: null } for a non-existent matter', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await confirmUpload(db, 'nonexistent', 'org-x')
expect(result.matter).toBeNull()
expect(result.quotaExceeded).toBeUndefined()
})
it('returns { matter: null } for a matter not in draft status', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-conf3' })
const now = Date.now()
@@ -201,7 +201,7 @@ describe('confirmUpload', () => {
})
it('returns { matter: null, quotaExceeded: true } when quota would be exceeded', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-conf4', used: 0 })
// quota=100, used=90, file size=50 → would exceed
@@ -215,7 +215,7 @@ describe('confirmUpload', () => {
})
it('status remains draft in DB when quota would be exceeded', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-conf5', used: 0 })
await insertOrgQuota(db, orgId, 100, 90)
@@ -232,7 +232,7 @@ describe('confirmUpload', () => {
describe('updateMatter', () => {
it('throws when moving a folder into itself', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-upd1' })
const now = Date.now()
@@ -248,7 +248,7 @@ describe('updateMatter', () => {
})
it('throws when moving a folder into its own subfolder', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-upd2' })
const now = Date.now()
@@ -279,7 +279,7 @@ async function insertTrashedMatter(
describe('listTrashedRoots', () => {
it('returns only the top-level trashed folder when a folder and its child file are both trashed', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-trash1' })
@@ -308,7 +308,7 @@ describe('listTrashedRoots', () => {
})
it('does not return the child file when it is nested inside a trashed folder', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-trash2' })
@@ -336,7 +336,7 @@ describe('listTrashedRoots', () => {
})
it('returns multiple independent trashed items when none is a descendant of another', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-trash3' })
@@ -366,7 +366,7 @@ describe('listTrashedRoots', () => {
})
it('returns an empty array when no trashed items exist for the org', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const roots = await listTrashedRoots(db, orgId)
@@ -375,7 +375,7 @@ describe('listTrashedRoots', () => {
})
it('excludes items belonging to a different org', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const otherOrgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-trash4' })
@@ -395,7 +395,7 @@ describe('listTrashedRoots', () => {
})
it('returns deeply-nested trashed folders that are themselves roots (parent folder not trashed)', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const orgId = nanoid()
const storageId = await insertStorage(db, { id: 'st-trash5' })
+6 -6
View File
@@ -4,7 +4,7 @@ import * as authSchema from '../db/auth-schema.js'
import { createTestApp } from '../test/setup.js'
import { findPersonalOrg } from './org.js'
type TestDb = ReturnType<typeof createTestApp>['db']
type TestDb = Awaited<ReturnType<typeof createTestApp>>['db']
async function insertUser(db: TestDb, overrides: Partial<{ id: string; name: string; email: string }> = {}) {
const id = overrides.id ?? nanoid()
@@ -43,7 +43,7 @@ async function insertMember(db: TestDb, organizationId: string, userId: string)
describe('findPersonalOrg', () => {
it('returns the org id when a personal org exists for the user', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const userId = await insertUser(db)
const orgId = await insertOrg(db, { slug: `personal-${userId}` })
await insertMember(db, orgId, userId)
@@ -53,7 +53,7 @@ describe('findPersonalOrg', () => {
})
it('returns null when user has no memberships', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const userId = await insertUser(db)
const result = await findPersonalOrg(db, userId)
@@ -61,7 +61,7 @@ describe('findPersonalOrg', () => {
})
it("returns null when the org's slug is not the user's personal slug", async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const userId = await insertUser(db)
const orgId = await insertOrg(db, { slug: 'some-team-org' })
await insertMember(db, orgId, userId)
@@ -71,7 +71,7 @@ describe('findPersonalOrg', () => {
})
it('finds the personal org among multiple memberships', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const userId = await insertUser(db)
const teamOrgId = await insertOrg(db, { slug: 'some-team-org' })
const personalOrgId = await insertOrg(db, { slug: `personal-${userId}` })
@@ -83,7 +83,7 @@ describe('findPersonalOrg', () => {
})
it('returns null when the personal slug exists but member row was deleted', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const userId = await insertUser(db)
await insertOrg(db, { slug: `personal-${userId}` })
// No member row inserted — membership is load-bearing
+25 -25
View File
@@ -5,7 +5,7 @@ import { createStorage, deleteStorage, getStorage, listStorages, selectStorage,
describe('createStorage', () => {
it('sets filePath to empty string regardless of input', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await createStorage(db, {
title: 'My Storage',
mode: 'private',
@@ -20,7 +20,7 @@ describe('createStorage', () => {
})
it('sets customHost to empty string when not provided', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await createStorage(db, {
title: 'My Storage',
mode: 'private',
@@ -35,7 +35,7 @@ describe('createStorage', () => {
})
it('uses provided customHost when given', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await createStorage(db, {
title: 'My Storage',
mode: 'private',
@@ -51,7 +51,7 @@ describe('createStorage', () => {
})
it('sets capacity to 0 when not provided', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await createStorage(db, {
title: 'My Storage',
mode: 'private',
@@ -66,7 +66,7 @@ describe('createStorage', () => {
})
it('uses provided capacity when given', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await createStorage(db, {
title: 'My Storage',
mode: 'private',
@@ -81,7 +81,7 @@ describe('createStorage', () => {
})
it('initialises used to 0 and status to active', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await createStorage(db, {
title: 'My Storage',
mode: 'public',
@@ -97,7 +97,7 @@ describe('createStorage', () => {
})
it('persists the created row to the database', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await createStorage(db, {
title: 'Persisted',
mode: 'private',
@@ -115,7 +115,7 @@ describe('createStorage', () => {
})
describe('updateStorage', () => {
async function seed(db: ReturnType<typeof createTestApp>['db']) {
async function seed(db: Awaited<ReturnType<typeof createTestApp>>['db']) {
return createStorage(db, {
title: 'Original',
mode: 'private',
@@ -130,13 +130,13 @@ describe('updateStorage', () => {
}
it('returns null when storage does not exist', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await updateStorage(db, 'nonexistent', { title: 'New' })
expect(result).toBeNull()
})
it('keeps existing values for fields not included in update', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await seed(db)
const updated = await updateStorage(db, created.id, { title: 'Changed' })
expect(updated?.bucket).toBe('original-bucket')
@@ -148,7 +148,7 @@ describe('updateStorage', () => {
})
it('applies all provided optional fields', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await seed(db)
const updated = await updateStorage(db, created.id, {
title: 'Updated',
@@ -175,7 +175,7 @@ describe('updateStorage', () => {
})
it('updates only status leaving all other fields intact', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await seed(db)
const updated = await updateStorage(db, created.id, { status: 'disabled' })
expect(updated?.status).toBe('disabled')
@@ -183,7 +183,7 @@ describe('updateStorage', () => {
})
it('updates the updatedAt timestamp', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await seed(db)
const before = created.updatedAt.getTime()
await new Promise((r) => setTimeout(r, 10))
@@ -194,13 +194,13 @@ describe('updateStorage', () => {
describe('listStorages', () => {
it('returns empty items and zero total when no storages exist', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await listStorages(db)
expect(result).toEqual({ items: [], total: 0 })
})
it('returns all storages ordered by createdAt ascending', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await createStorage(db, {
title: 'First',
mode: 'private',
@@ -229,13 +229,13 @@ describe('listStorages', () => {
describe('getStorage', () => {
it('returns null when storage does not exist', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await getStorage(db, 'nonexistent')
expect(result).toBeNull()
})
it('returns the storage when it exists', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await createStorage(db, {
title: 'Findable',
mode: 'private',
@@ -253,7 +253,7 @@ describe('getStorage', () => {
describe('selectStorage', () => {
async function seedActive(
db: ReturnType<typeof createTestApp>['db'],
db: Awaited<ReturnType<typeof createTestApp>>['db'],
mode: 'private' | 'public',
opts: { capacity?: number; used?: number; status?: string } = {},
) {
@@ -270,26 +270,26 @@ describe('selectStorage', () => {
}
it('returns an active private storage with unlimited capacity', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await seedActive(db, 'private')
const found = await selectStorage(db, 'private')
expect(found.id).toBe(created.id)
})
it('returns an active public storage when requested', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await seedActive(db, 'public')
const found = await selectStorage(db, 'public')
expect(found.id).toBe(created.id)
})
it('throws when no active storage exists for the requested mode', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await expect(selectStorage(db, 'private')).rejects.toThrow('No available storage')
})
it('throws when storage is present but mode does not match', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
await seedActive(db, 'public')
await expect(selectStorage(db, 'private')).rejects.toThrow('No available storage')
})
@@ -297,13 +297,13 @@ describe('selectStorage', () => {
describe('deleteStorage', () => {
it('returns not_found when storage does not exist', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const result = await deleteStorage(db, 'nonexistent')
expect(result).toBe('not_found')
})
it('deletes a storage that is not referenced by any matter', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await createStorage(db, {
title: 'Deletable',
mode: 'private',
@@ -320,7 +320,7 @@ describe('deleteStorage', () => {
})
it('returns in_use when matters reference the storage', async () => {
const { db } = createTestApp()
const { db } = await createTestApp()
const created = await createStorage(db, {
title: 'In Use',
mode: 'private',
+2 -2
View File
@@ -148,7 +148,7 @@ const APP_SCHEMA_SQL = `
);
`
export function createTestApp() {
export async function createTestApp() {
const sqlite = new Database(':memory:')
sqlite.exec(AUTH_SCHEMA_SQL)
sqlite.exec(APP_SCHEMA_SQL)
@@ -158,7 +158,7 @@ export function createTestApp() {
db,
getEnv: () => undefined,
}
const auth = createAuth(db, 'test-secret', 'http://localhost:3000')
const auth = await createAuth(db, 'test-secret', 'http://localhost:3000')
const app = createApp(platform, auth)
return { app, db, auth }
+79
View File
@@ -0,0 +1,79 @@
/**
* Static display metadata for all built-in better-auth social providers.
* Used by the frontend to render login buttons with correct names and icons.
*/
export const OAuthProviderMeta: Record<string, { name: string; icon: string } | undefined> = {
apple: { name: 'Apple', icon: 'apple' },
atlassian: { name: 'Atlassian', icon: 'atlassian' },
cognito: { name: 'AWS Cognito', icon: 'cognito' },
discord: { name: 'Discord', icon: 'discord' },
dropbox: { name: 'Dropbox', icon: 'dropbox' },
facebook: { name: 'Facebook', icon: 'facebook' },
figma: { name: 'Figma', icon: 'figma' },
github: { name: 'GitHub', icon: 'github' },
gitlab: { name: 'GitLab', icon: 'gitlab' },
google: { name: 'Google', icon: 'google' },
huggingface: { name: 'Hugging Face', icon: 'huggingface' },
kakao: { name: 'Kakao', icon: 'kakao' },
kick: { name: 'Kick', icon: 'kick' },
line: { name: 'LINE', icon: 'line' },
linear: { name: 'Linear', icon: 'linear' },
linkedin: { name: 'LinkedIn', icon: 'linkedin' },
microsoft: { name: 'Microsoft', icon: 'microsoft' },
naver: { name: 'Naver', icon: 'naver' },
notion: { name: 'Notion', icon: 'notion' },
paybin: { name: 'Paybin', icon: 'paybin' },
paypal: { name: 'PayPal', icon: 'paypal' },
polar: { name: 'Polar', icon: 'polar' },
railway: { name: 'Railway', icon: 'railway' },
reddit: { name: 'Reddit', icon: 'reddit' },
roblox: { name: 'Roblox', icon: 'roblox' },
salesforce: { name: 'Salesforce', icon: 'salesforce' },
slack: { name: 'Slack', icon: 'slack' },
spotify: { name: 'Spotify', icon: 'spotify' },
tiktok: { name: 'TikTok', icon: 'tiktok' },
twitch: { name: 'Twitch', icon: 'twitch' },
twitter: { name: 'Twitter / X', icon: 'twitter' },
vercel: { name: 'Vercel', icon: 'vercel' },
vk: { name: 'VK', icon: 'vk' },
wechat: { name: 'WeChat', icon: 'wechat' },
zoom: { name: 'Zoom', icon: 'zoom' },
}
/** All built-in provider IDs supported by better-auth */
export const BUILTIN_PROVIDER_IDS = Object.keys(OAuthProviderMeta) as readonly string[]
/** Key prefix for OAuth provider configs stored in system_options */
export const OAUTH_PROVIDER_KEY_PREFIX = 'oauth_provider_'
/** LIKE pattern for querying all OAuth provider configs */
export const OAUTH_PROVIDER_KEY_PATTERN = `${OAUTH_PROVIDER_KEY_PREFIX}%`
/** Regex for valid custom OIDC provider IDs */
const PROVIDER_ID_RE = /^[a-z0-9-]+$/
export function isValidProviderId(id: string): boolean {
return PROVIDER_ID_RE.test(id)
}
export function parseProviderConfig(value: string): OAuthProviderConfig | null {
try {
return JSON.parse(value) as OAuthProviderConfig
} catch {
return null
}
}
export type OAuthProviderType = 'builtin' | 'oidc'
export interface OAuthProviderConfig {
providerId: string
type: OAuthProviderType
clientId: string
clientSecret: string
enabled: boolean
/** Only for type: 'oidc' */
discoveryUrl?: string
/** Only for type: 'oidc' */
scopes?: string[]
}
+18 -8
View File
@@ -1,4 +1,5 @@
import { createApp } from '../server/app'
import type { Auth } from '../server/auth'
import { createAuth } from '../server/auth'
import { createCloudflarePlatform } from '../server/platform/cloudflare'
@@ -10,19 +11,28 @@ interface Env {
[key: string]: unknown
}
// Cache auth instance at isolate scope to avoid per-request DB queries
// for OIDC config loading. Changes to OIDC provider configs or env vars
// (BETTER_AUTH_URL, TRUSTED_ORIGINS) take effect on isolate recycle.
let cachedAuth: Auth | null = null
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { BETTER_AUTH_SECRET, BETTER_AUTH_URL, TRUSTED_ORIGINS } = env
const { BETTER_AUTH_SECRET } = env
if (!BETTER_AUTH_SECRET) {
throw new Error('BETTER_AUTH_SECRET is not configured for this deployment.')
}
const origin = new URL(request.url).origin
const baseURL = BETTER_AUTH_URL || origin
const platform = createCloudflarePlatform(env)
const trustedOrigins = TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || [origin]
const auth = createAuth(platform.db, BETTER_AUTH_SECRET, baseURL, trustedOrigins)
return createApp(platform, auth).fetch(request)
if (!cachedAuth) {
const origin = new URL(request.url).origin
const baseURL = env.BETTER_AUTH_URL || origin
const trustedOrigins = env.TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || [origin]
cachedAuth = await createAuth(platform.db, BETTER_AUTH_SECRET, baseURL, trustedOrigins)
}
return createApp(platform, cachedAuth).fetch(request)
},
}