@vobase/core 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -9
- package/src/__tests__/drizzle-introspection.test.ts +77 -0
- package/src/__tests__/e2e.test.ts +225 -0
- package/src/__tests__/permissions.test.ts +157 -0
- package/src/__tests__/rpc-types.test.ts +92 -0
- package/src/app.test.ts +99 -0
- package/src/app.ts +178 -0
- package/src/audit.test.ts +126 -0
- package/src/auth.test.ts +74 -0
- package/src/contracts/auth.ts +37 -0
- package/{dist/contracts/module.d.ts → src/contracts/module.ts} +6 -6
- package/src/contracts/notify.ts +47 -0
- package/src/contracts/permissions.ts +10 -0
- package/src/contracts/storage.ts +61 -0
- package/src/ctx.test.ts +162 -0
- package/src/ctx.ts +64 -0
- package/src/db/client.test.ts +75 -0
- package/src/db/client.ts +15 -0
- package/src/db/helpers.test.ts +147 -0
- package/src/db/helpers.ts +51 -0
- package/src/db/index.ts +8 -0
- package/{dist/index.d.ts → src/index.ts} +103 -6
- package/src/infra/circuit-breaker.test.ts +74 -0
- package/src/infra/circuit-breaker.ts +57 -0
- package/src/infra/errors.test.ts +175 -0
- package/src/infra/errors.ts +64 -0
- package/src/infra/http-client.test.ts +482 -0
- package/src/infra/http-client.ts +221 -0
- package/src/infra/index.ts +35 -0
- package/src/infra/job.test.ts +85 -0
- package/src/infra/job.ts +94 -0
- package/src/infra/logger.test.ts +65 -0
- package/src/infra/logger.ts +18 -0
- package/src/infra/queue.test.ts +46 -0
- package/src/infra/queue.ts +147 -0
- package/src/infra/throw-proxy.test.ts +34 -0
- package/src/infra/throw-proxy.ts +17 -0
- package/src/infra/webhooks-schema.ts +17 -0
- package/src/infra/webhooks.test.ts +364 -0
- package/src/infra/webhooks.ts +146 -0
- package/src/mcp/auth.test.ts +129 -0
- package/src/mcp/crud.test.ts +128 -0
- package/src/mcp/crud.ts +171 -0
- package/{dist/mcp/index.d.ts → src/mcp/index.ts} +0 -1
- package/src/mcp/server.test.ts +153 -0
- package/src/mcp/server.ts +178 -0
- package/src/middleware/audit.test.ts +169 -0
- package/src/module-registry.ts +18 -0
- package/src/module.test.ts +168 -0
- package/src/module.ts +111 -0
- package/src/modules/audit/index.ts +18 -0
- package/src/modules/audit/middleware.ts +33 -0
- package/src/modules/audit/schema.ts +35 -0
- package/src/modules/audit/track-changes.ts +70 -0
- package/src/modules/auth/audit-hooks.ts +74 -0
- package/src/modules/auth/index.ts +101 -0
- package/src/modules/auth/middleware.ts +51 -0
- package/src/modules/auth/permissions.ts +46 -0
- package/src/modules/auth/schema.ts +184 -0
- package/src/modules/credentials/encrypt.ts +95 -0
- package/src/modules/credentials/index.ts +15 -0
- package/src/modules/credentials/schema.ts +10 -0
- package/src/modules/notify/index.ts +90 -0
- package/src/modules/notify/notify.test.ts +145 -0
- package/src/modules/notify/providers/resend.ts +47 -0
- package/src/modules/notify/providers/smtp.ts +117 -0
- package/src/modules/notify/providers/waba.ts +82 -0
- package/src/modules/notify/schema.ts +27 -0
- package/src/modules/notify/service.ts +93 -0
- package/src/modules/sequences/index.ts +15 -0
- package/src/modules/sequences/next-sequence.ts +48 -0
- package/src/modules/sequences/schema.ts +12 -0
- package/src/modules/storage/index.ts +44 -0
- package/src/modules/storage/providers/local.ts +124 -0
- package/src/modules/storage/providers/s3.ts +83 -0
- package/src/modules/storage/routes.ts +76 -0
- package/src/modules/storage/schema.ts +26 -0
- package/src/modules/storage/service.ts +202 -0
- package/src/modules/storage/storage.test.ts +225 -0
- package/src/schemas.test.ts +44 -0
- package/src/schemas.ts +63 -0
- package/src/sequence.test.ts +56 -0
- package/dist/app.d.ts +0 -37
- package/dist/app.d.ts.map +0 -1
- package/dist/contracts/auth.d.ts +0 -35
- package/dist/contracts/auth.d.ts.map +0 -1
- package/dist/contracts/module.d.ts.map +0 -1
- package/dist/contracts/notify.d.ts +0 -46
- package/dist/contracts/notify.d.ts.map +0 -1
- package/dist/contracts/permissions.d.ts +0 -10
- package/dist/contracts/permissions.d.ts.map +0 -1
- package/dist/contracts/storage.d.ts +0 -54
- package/dist/contracts/storage.d.ts.map +0 -1
- package/dist/ctx.d.ts +0 -40
- package/dist/ctx.d.ts.map +0 -1
- package/dist/db/client.d.ts +0 -4
- package/dist/db/client.d.ts.map +0 -1
- package/dist/db/helpers.d.ts +0 -26
- package/dist/db/helpers.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -3
- package/dist/db/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -98611
- package/dist/infra/circuit-breaker.d.ts +0 -17
- package/dist/infra/circuit-breaker.d.ts.map +0 -1
- package/dist/infra/errors.d.ts +0 -26
- package/dist/infra/errors.d.ts.map +0 -1
- package/dist/infra/http-client.d.ts +0 -31
- package/dist/infra/http-client.d.ts.map +0 -1
- package/dist/infra/index.d.ts +0 -11
- package/dist/infra/index.d.ts.map +0 -1
- package/dist/infra/job.d.ts +0 -14
- package/dist/infra/job.d.ts.map +0 -1
- package/dist/infra/logger.d.ts +0 -7
- package/dist/infra/logger.d.ts.map +0 -1
- package/dist/infra/queue.d.ts +0 -18
- package/dist/infra/queue.d.ts.map +0 -1
- package/dist/infra/throw-proxy.d.ts +0 -7
- package/dist/infra/throw-proxy.d.ts.map +0 -1
- package/dist/infra/webhooks-schema.d.ts +0 -60
- package/dist/infra/webhooks-schema.d.ts.map +0 -1
- package/dist/infra/webhooks.d.ts +0 -46
- package/dist/infra/webhooks.d.ts.map +0 -1
- package/dist/mcp/crud.d.ts +0 -12
- package/dist/mcp/crud.d.ts.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/server.d.ts +0 -16
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/module-registry.d.ts +0 -3
- package/dist/module-registry.d.ts.map +0 -1
- package/dist/module.d.ts +0 -29
- package/dist/module.d.ts.map +0 -1
- package/dist/modules/audit/index.d.ts +0 -5
- package/dist/modules/audit/index.d.ts.map +0 -1
- package/dist/modules/audit/middleware.d.ts +0 -3
- package/dist/modules/audit/middleware.d.ts.map +0 -1
- package/dist/modules/audit/schema.d.ts +0 -247
- package/dist/modules/audit/schema.d.ts.map +0 -1
- package/dist/modules/audit/track-changes.d.ts +0 -3
- package/dist/modules/audit/track-changes.d.ts.map +0 -1
- package/dist/modules/auth/audit-hooks.d.ts +0 -6
- package/dist/modules/auth/audit-hooks.d.ts.map +0 -1
- package/dist/modules/auth/index.d.ts +0 -25
- package/dist/modules/auth/index.d.ts.map +0 -1
- package/dist/modules/auth/middleware.d.ts +0 -15
- package/dist/modules/auth/middleware.d.ts.map +0 -1
- package/dist/modules/auth/permissions.d.ts +0 -5
- package/dist/modules/auth/permissions.d.ts.map +0 -1
- package/dist/modules/auth/schema.d.ts +0 -2519
- package/dist/modules/auth/schema.d.ts.map +0 -1
- package/dist/modules/credentials/encrypt.d.ts +0 -12
- package/dist/modules/credentials/encrypt.d.ts.map +0 -1
- package/dist/modules/credentials/index.d.ts +0 -4
- package/dist/modules/credentials/index.d.ts.map +0 -1
- package/dist/modules/credentials/schema.d.ts +0 -56
- package/dist/modules/credentials/schema.d.ts.map +0 -1
- package/dist/modules/notify/index.d.ts +0 -36
- package/dist/modules/notify/index.d.ts.map +0 -1
- package/dist/modules/notify/providers/resend.d.ts +0 -7
- package/dist/modules/notify/providers/resend.d.ts.map +0 -1
- package/dist/modules/notify/providers/smtp.d.ts +0 -18
- package/dist/modules/notify/providers/smtp.d.ts.map +0 -1
- package/dist/modules/notify/providers/waba.d.ts +0 -12
- package/dist/modules/notify/providers/waba.d.ts.map +0 -1
- package/dist/modules/notify/schema.d.ts +0 -337
- package/dist/modules/notify/schema.d.ts.map +0 -1
- package/dist/modules/notify/service.d.ts +0 -22
- package/dist/modules/notify/service.d.ts.map +0 -1
- package/dist/modules/sequences/index.d.ts +0 -4
- package/dist/modules/sequences/index.d.ts.map +0 -1
- package/dist/modules/sequences/next-sequence.d.ts +0 -8
- package/dist/modules/sequences/next-sequence.d.ts.map +0 -1
- package/dist/modules/sequences/schema.d.ts +0 -72
- package/dist/modules/sequences/schema.d.ts.map +0 -1
- package/dist/modules/storage/index.d.ts +0 -24
- package/dist/modules/storage/index.d.ts.map +0 -1
- package/dist/modules/storage/providers/local.d.ts +0 -3
- package/dist/modules/storage/providers/local.d.ts.map +0 -1
- package/dist/modules/storage/providers/s3.d.ts +0 -3
- package/dist/modules/storage/providers/s3.d.ts.map +0 -1
- package/dist/modules/storage/routes.d.ts +0 -4
- package/dist/modules/storage/routes.d.ts.map +0 -1
- package/dist/modules/storage/schema.d.ts +0 -273
- package/dist/modules/storage/schema.d.ts.map +0 -1
- package/dist/modules/storage/service.d.ts +0 -35
- package/dist/modules/storage/service.d.ts.map +0 -1
- package/dist/schemas.d.ts +0 -19
- package/dist/schemas.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vobase/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The app framework built for AI coding agents - core runtime",
|
|
6
6
|
"repository": {
|
|
@@ -9,24 +9,22 @@
|
|
|
9
9
|
"directory": "packages/core"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "./
|
|
13
|
-
"types": "./
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
|
-
"import": "./
|
|
17
|
-
"
|
|
18
|
-
"types": "./dist/index.d.ts"
|
|
16
|
+
"import": "./src/index.ts",
|
|
17
|
+
"types": "./src/index.ts"
|
|
19
18
|
}
|
|
20
19
|
},
|
|
21
20
|
"files": [
|
|
22
|
-
"
|
|
21
|
+
"src"
|
|
23
22
|
],
|
|
24
23
|
"publishConfig": {
|
|
25
24
|
"access": "public"
|
|
26
25
|
},
|
|
27
26
|
"scripts": {
|
|
28
|
-
"build": "
|
|
29
|
-
"dev": "bun run build --watch",
|
|
27
|
+
"build": "tsc --noEmit",
|
|
30
28
|
"test": "bun test src/",
|
|
31
29
|
"lint": "biome lint src/",
|
|
32
30
|
"typecheck": "tsc --noEmit"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { getTableColumns, getTableName } from 'drizzle-orm';
|
|
3
|
+
|
|
4
|
+
import { auditLog } from '../modules/audit/schema';
|
|
5
|
+
import { authUser } from '../modules/auth/schema';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Drizzle Introspection Spike (US-005)
|
|
9
|
+
*
|
|
10
|
+
* Validates that Drizzle table column metadata (types, nullability, defaults,
|
|
11
|
+
* primary keys) is extractable via getTableColumns(). This API is used in
|
|
12
|
+
* Step 2.2 (MCP CRUD generation) to build Zod input schemas dynamically.
|
|
13
|
+
*
|
|
14
|
+
* API reference for MCP CRUD generation:
|
|
15
|
+
* - getTableColumns(table) → Record<string, Column> with .dataType, .notNull, .hasDefault, .primary, .columnType
|
|
16
|
+
* - getTableName(table) → string (the SQL table name)
|
|
17
|
+
*/
|
|
18
|
+
describe('Drizzle table introspection', () => {
|
|
19
|
+
it('extracts column names from a table', () => {
|
|
20
|
+
const columns = getTableColumns(auditLog);
|
|
21
|
+
const names = Object.keys(columns);
|
|
22
|
+
expect(names).toContain('id');
|
|
23
|
+
expect(names).toContain('event');
|
|
24
|
+
expect(names).toContain('actorId');
|
|
25
|
+
expect(names).toContain('createdAt');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('extracts SQL data types', () => {
|
|
29
|
+
const columns = getTableColumns(auditLog);
|
|
30
|
+
expect(columns.id.dataType).toBe('string'); // text
|
|
31
|
+
expect(columns.event.dataType).toBe('string'); // text
|
|
32
|
+
// timestamp_ms mode columns report as 'object date', not 'number'
|
|
33
|
+
expect(columns.createdAt.dataType).toBe('object date');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('extracts columnType for precise SQL type', () => {
|
|
37
|
+
const columns = getTableColumns(auditLog);
|
|
38
|
+
expect(columns.id.columnType).toBe('SQLiteText');
|
|
39
|
+
// timestamp_ms columns use SQLiteTimestamp, not SQLiteInteger
|
|
40
|
+
expect(columns.createdAt.columnType).toBe('SQLiteTimestamp');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('extracts nullability', () => {
|
|
44
|
+
const columns = getTableColumns(auditLog);
|
|
45
|
+
expect(columns.event.notNull).toBe(true);
|
|
46
|
+
expect(columns.actorId.notNull).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('extracts hasDefault flag', () => {
|
|
50
|
+
const columns = getTableColumns(auditLog);
|
|
51
|
+
// id has a $defaultFn (nanoid)
|
|
52
|
+
expect(columns.id.hasDefault).toBe(true);
|
|
53
|
+
// event has no default
|
|
54
|
+
expect(columns.event.hasDefault).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('extracts primary key flag via .primary', () => {
|
|
58
|
+
const columns = getTableColumns(auditLog);
|
|
59
|
+
expect((columns.id as any).primary).toBe(true);
|
|
60
|
+
expect((columns.event as any).primary).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('extracts SQL table name', () => {
|
|
64
|
+
const name = getTableName(auditLog);
|
|
65
|
+
expect(name).toBe('_audit_log');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('works on auth user table with all field types', () => {
|
|
69
|
+
const columns = getTableColumns(authUser);
|
|
70
|
+
expect((columns.id as any).primary).toBe(true);
|
|
71
|
+
expect(columns.email.notNull).toBe(true);
|
|
72
|
+
expect(columns.image.notNull).toBe(false);
|
|
73
|
+
expect(columns.role.hasDefault).toBe(true);
|
|
74
|
+
// integer boolean uses 'boolean' dataType in Drizzle
|
|
75
|
+
expect(columns.emailVerified.columnType).toBe('SQLiteBoolean');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { Database } from 'bun:sqlite';
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
|
|
4
|
+
import { shutdownManager } from 'bunqueue/client';
|
|
5
|
+
|
|
6
|
+
import { createApp } from '../app';
|
|
7
|
+
import { createDatabase, type VobaseDb } from '../db';
|
|
8
|
+
type DbWithClient = VobaseDb & { $client: Database };
|
|
9
|
+
|
|
10
|
+
const dbPath = `/tmp/vobase-e2e-${process.pid}-${Date.now()}.db`;
|
|
11
|
+
const queueDbPath = dbPath.replace(/\.db$/, '-queue.db');
|
|
12
|
+
const email = `e2e-${Date.now()}@test.com`;
|
|
13
|
+
const password = 'Test1234!';
|
|
14
|
+
|
|
15
|
+
let app: Awaited<ReturnType<typeof createApp>>;
|
|
16
|
+
let systemDb: DbWithClient;
|
|
17
|
+
let sessionCookie = '';
|
|
18
|
+
let previousAuthSecret: string | undefined;
|
|
19
|
+
let previousAuthUrl: string | undefined;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create all required tables for the e2e test.
|
|
23
|
+
* Since ensureCoreTables() was removed, we create tables via raw SQL
|
|
24
|
+
* matching the Drizzle schema definitions.
|
|
25
|
+
*/
|
|
26
|
+
function createTables(db: Database) {
|
|
27
|
+
db.run(`CREATE TABLE IF NOT EXISTS "user" (
|
|
28
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
29
|
+
"name" text NOT NULL,
|
|
30
|
+
"email" text NOT NULL,
|
|
31
|
+
"email_verified" integer NOT NULL DEFAULT 0,
|
|
32
|
+
"image" text,
|
|
33
|
+
"role" text NOT NULL DEFAULT 'user',
|
|
34
|
+
"created_at" integer NOT NULL,
|
|
35
|
+
"updated_at" integer NOT NULL
|
|
36
|
+
)`);
|
|
37
|
+
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS "user_email_unique" ON "user" ("email")`);
|
|
38
|
+
|
|
39
|
+
db.run(`CREATE TABLE IF NOT EXISTS "session" (
|
|
40
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
41
|
+
"expires_at" integer NOT NULL,
|
|
42
|
+
"token" text NOT NULL,
|
|
43
|
+
"created_at" integer NOT NULL,
|
|
44
|
+
"updated_at" integer NOT NULL,
|
|
45
|
+
"ip_address" text,
|
|
46
|
+
"user_agent" text,
|
|
47
|
+
"user_id" text NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
|
48
|
+
)`);
|
|
49
|
+
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS "session_token_unique" ON "session" ("token")`);
|
|
50
|
+
db.run(`CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "session" ("user_id")`);
|
|
51
|
+
|
|
52
|
+
db.run(`CREATE TABLE IF NOT EXISTS "account" (
|
|
53
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
54
|
+
"account_id" text NOT NULL,
|
|
55
|
+
"provider_id" text NOT NULL,
|
|
56
|
+
"user_id" text NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
|
57
|
+
"access_token" text,
|
|
58
|
+
"refresh_token" text,
|
|
59
|
+
"id_token" text,
|
|
60
|
+
"access_token_expires_at" integer,
|
|
61
|
+
"refresh_token_expires_at" integer,
|
|
62
|
+
"scope" text,
|
|
63
|
+
"password" text,
|
|
64
|
+
"created_at" integer NOT NULL,
|
|
65
|
+
"updated_at" integer NOT NULL
|
|
66
|
+
)`);
|
|
67
|
+
db.run(`CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "account" ("user_id")`);
|
|
68
|
+
|
|
69
|
+
db.run(`CREATE TABLE IF NOT EXISTS "verification" (
|
|
70
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
71
|
+
"identifier" text NOT NULL,
|
|
72
|
+
"value" text NOT NULL,
|
|
73
|
+
"expires_at" integer NOT NULL,
|
|
74
|
+
"created_at" integer NOT NULL,
|
|
75
|
+
"updated_at" integer NOT NULL
|
|
76
|
+
)`);
|
|
77
|
+
db.run(`CREATE INDEX IF NOT EXISTS "verification_identifier_idx" ON "verification" ("identifier")`);
|
|
78
|
+
|
|
79
|
+
db.run(`CREATE TABLE IF NOT EXISTS "_audit_log" (
|
|
80
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
81
|
+
"event" text NOT NULL,
|
|
82
|
+
"actor_id" text,
|
|
83
|
+
"actor_email" text,
|
|
84
|
+
"ip" text,
|
|
85
|
+
"details" text,
|
|
86
|
+
"created_at" integer NOT NULL
|
|
87
|
+
)`);
|
|
88
|
+
|
|
89
|
+
db.run(`CREATE TABLE IF NOT EXISTS "_record_audits" (
|
|
90
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
91
|
+
"table_name" text NOT NULL,
|
|
92
|
+
"record_id" text NOT NULL,
|
|
93
|
+
"old_data" text,
|
|
94
|
+
"new_data" text,
|
|
95
|
+
"changed_by" text,
|
|
96
|
+
"created_at" integer NOT NULL
|
|
97
|
+
)`);
|
|
98
|
+
|
|
99
|
+
db.run(`CREATE TABLE IF NOT EXISTS "_sequences" (
|
|
100
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
101
|
+
"prefix" text NOT NULL,
|
|
102
|
+
"current_value" integer NOT NULL DEFAULT 0,
|
|
103
|
+
"updated_at" integer NOT NULL
|
|
104
|
+
)`);
|
|
105
|
+
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS "_sequences_prefix_unique" ON "_sequences" ("prefix")`);
|
|
106
|
+
|
|
107
|
+
db.run(`CREATE TABLE IF NOT EXISTS "_webhook_dedup" (
|
|
108
|
+
"id" text NOT NULL,
|
|
109
|
+
"source" text NOT NULL,
|
|
110
|
+
"received_at" integer NOT NULL,
|
|
111
|
+
PRIMARY KEY ("id", "source")
|
|
112
|
+
)`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const getPragmaValue = (db: DbWithClient, pragma: string): string => {
|
|
116
|
+
const row = db.$client.query(`PRAGMA ${pragma}`).get() as Record<
|
|
117
|
+
string,
|
|
118
|
+
unknown
|
|
119
|
+
>;
|
|
120
|
+
return String(Object.values(row)[0]);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
beforeAll(async () => {
|
|
124
|
+
previousAuthSecret = process.env.BETTER_AUTH_SECRET;
|
|
125
|
+
previousAuthUrl = process.env.BETTER_AUTH_URL;
|
|
126
|
+
process.env.BETTER_AUTH_SECRET = 'vobase-e2e-secret';
|
|
127
|
+
process.env.BETTER_AUTH_URL = 'http://localhost';
|
|
128
|
+
|
|
129
|
+
// Create tables before createApp opens its own connection
|
|
130
|
+
const bootstrapDb = new Database(dbPath);
|
|
131
|
+
createTables(bootstrapDb);
|
|
132
|
+
bootstrapDb.close();
|
|
133
|
+
|
|
134
|
+
systemDb = createDatabase(dbPath) as DbWithClient;
|
|
135
|
+
app = await createApp({
|
|
136
|
+
database: dbPath,
|
|
137
|
+
modules: [],
|
|
138
|
+
mcp: { enabled: true },
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterAll(() => {
|
|
143
|
+
shutdownManager();
|
|
144
|
+
systemDb?.$client.close();
|
|
145
|
+
if (previousAuthSecret === undefined) delete process.env.BETTER_AUTH_SECRET;
|
|
146
|
+
else process.env.BETTER_AUTH_SECRET = previousAuthSecret;
|
|
147
|
+
if (previousAuthUrl === undefined) delete process.env.BETTER_AUTH_URL;
|
|
148
|
+
else process.env.BETTER_AUTH_URL = previousAuthUrl;
|
|
149
|
+
|
|
150
|
+
rmSync(dbPath, { force: true });
|
|
151
|
+
rmSync(`${dbPath}-wal`, { force: true });
|
|
152
|
+
rmSync(`${dbPath}-shm`, { force: true });
|
|
153
|
+
rmSync(queueDbPath, { force: true });
|
|
154
|
+
rmSync(`${queueDbPath}-wal`, { force: true });
|
|
155
|
+
rmSync(`${queueDbPath}-shm`, { force: true });
|
|
156
|
+
rmSync('./data/bunqueue.db', { force: true });
|
|
157
|
+
rmSync('./data/bunqueue.db-wal', { force: true });
|
|
158
|
+
rmSync('./data/bunqueue.db-shm', { force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('vobase engine e2e integration', () => {
|
|
162
|
+
it('passes health, auth, mcp, and db pragma checks', async () => {
|
|
163
|
+
const health = await app.request('http://localhost/health');
|
|
164
|
+
const healthBody = (await health.json()) as {
|
|
165
|
+
status: string;
|
|
166
|
+
uptime: number;
|
|
167
|
+
};
|
|
168
|
+
expect(health.status).toBe(200);
|
|
169
|
+
expect(healthBody).toMatchObject({ status: 'ok' });
|
|
170
|
+
expect(typeof healthBody.uptime).toBe('number');
|
|
171
|
+
|
|
172
|
+
const signup = await app.request(
|
|
173
|
+
'http://localhost/api/auth/sign-up/email',
|
|
174
|
+
{
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'content-type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({ email, password, name: 'E2E Test' }),
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
const signupBody = (await signup.json()) as { user?: { email?: string } };
|
|
181
|
+
expect(signup.status).toBe(200);
|
|
182
|
+
expect(signupBody.user?.email).toBe(email);
|
|
183
|
+
|
|
184
|
+
const signin = await app.request(
|
|
185
|
+
'http://localhost/api/auth/sign-in/email',
|
|
186
|
+
{
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: { 'content-type': 'application/json' },
|
|
189
|
+
body: JSON.stringify({ email, password }),
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
expect(signin.status).toBe(200);
|
|
193
|
+
sessionCookie =
|
|
194
|
+
(signin.headers.get('set-cookie') ?? '').split(';')[0] ?? '';
|
|
195
|
+
expect(sessionCookie.length).toBeGreaterThan(0);
|
|
196
|
+
|
|
197
|
+
const mcp = await app.request('http://localhost/mcp', {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: {
|
|
200
|
+
'content-type': 'application/json',
|
|
201
|
+
accept: 'application/json, text/event-stream',
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', id: 1 }),
|
|
204
|
+
});
|
|
205
|
+
expect(mcp.status).toBe(200);
|
|
206
|
+
expect(
|
|
207
|
+
Array.isArray(
|
|
208
|
+
((await mcp.json()) as { result?: { tools?: unknown[] } }).result
|
|
209
|
+
?.tools,
|
|
210
|
+
),
|
|
211
|
+
).toBe(true);
|
|
212
|
+
|
|
213
|
+
expect({
|
|
214
|
+
journalMode: getPragmaValue(systemDb, 'journal_mode'),
|
|
215
|
+
busyTimeout: getPragmaValue(systemDb, 'busy_timeout'),
|
|
216
|
+
synchronous: getPragmaValue(systemDb, 'synchronous'),
|
|
217
|
+
foreignKeys: getPragmaValue(systemDb, 'foreign_keys'),
|
|
218
|
+
}).toEqual({
|
|
219
|
+
journalMode: 'wal',
|
|
220
|
+
busyTimeout: '5000',
|
|
221
|
+
synchronous: '1',
|
|
222
|
+
foreignKeys: '1',
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'bun:test';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
|
|
4
|
+
import { requireRole, requirePermission, requireOrg } from '../modules/auth/permissions';
|
|
5
|
+
import { setOrganizationEnabled } from '../modules/auth/permissions';
|
|
6
|
+
|
|
7
|
+
function createTestApp() {
|
|
8
|
+
const app = new Hono();
|
|
9
|
+
// Simulate the context variable map user field
|
|
10
|
+
return app;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function withUser(app: Hono, user: { id: string; email: string; name: string; role: string; activeOrganizationId?: string; orgRole?: string } | null) {
|
|
14
|
+
app.use('*', async (c, next) => {
|
|
15
|
+
c.set('user', user as any);
|
|
16
|
+
await next();
|
|
17
|
+
});
|
|
18
|
+
return app;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('requireRole', () => {
|
|
22
|
+
it('allows user with matching role', async () => {
|
|
23
|
+
const app = createTestApp();
|
|
24
|
+
withUser(app, { id: '1', email: 'a@b.com', name: 'Test', role: 'admin' });
|
|
25
|
+
app.get('/test', requireRole('admin'), (c) => c.json({ ok: true }));
|
|
26
|
+
|
|
27
|
+
const res = await app.request('/test');
|
|
28
|
+
expect(res.status).toBe(200);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('allows user with any of multiple roles', async () => {
|
|
32
|
+
const app = createTestApp();
|
|
33
|
+
withUser(app, { id: '1', email: 'a@b.com', name: 'Test', role: 'editor' });
|
|
34
|
+
app.get('/test', requireRole('admin', 'editor'), (c) => c.json({ ok: true }));
|
|
35
|
+
|
|
36
|
+
const res = await app.request('/test');
|
|
37
|
+
expect(res.status).toBe(200);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects user with non-matching role with 403', async () => {
|
|
41
|
+
const app = createTestApp();
|
|
42
|
+
withUser(app, { id: '1', email: 'a@b.com', name: 'Test', role: 'user' });
|
|
43
|
+
app.get('/test', requireRole('admin'), (c) => c.json({ ok: true }));
|
|
44
|
+
app.onError((err, c) => c.json({ error: err.message }, 403));
|
|
45
|
+
|
|
46
|
+
const res = await app.request('/test');
|
|
47
|
+
expect(res.status).toBe(403);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects unauthenticated request (no user) with 403', async () => {
|
|
51
|
+
const app = createTestApp();
|
|
52
|
+
withUser(app, null);
|
|
53
|
+
app.get('/test', requireRole('admin'), (c) => c.json({ ok: true }));
|
|
54
|
+
app.onError((err, c) => c.json({ error: err.message }, 403));
|
|
55
|
+
|
|
56
|
+
const res = await app.request('/test');
|
|
57
|
+
expect(res.status).toBe(403);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('requirePermission', () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
setOrganizationEnabled(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('throws descriptive error at call time when org is NOT enabled', () => {
|
|
67
|
+
setOrganizationEnabled(false);
|
|
68
|
+
expect(() => requirePermission('invoices:write')).toThrow(
|
|
69
|
+
'Organization plugin required for permission-based auth. Use requireRole() instead or enable organization in config.',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns middleware when org IS enabled', () => {
|
|
74
|
+
setOrganizationEnabled(true);
|
|
75
|
+
const middleware = requirePermission('invoices:write');
|
|
76
|
+
expect(typeof middleware).toBe('function');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('middleware rejects unauthenticated request when org is enabled', async () => {
|
|
80
|
+
setOrganizationEnabled(true);
|
|
81
|
+
const app = createTestApp();
|
|
82
|
+
withUser(app, null);
|
|
83
|
+
app.get('/test', requirePermission('invoices:write'), (c) => c.json({ ok: true }));
|
|
84
|
+
app.onError((err, c) => c.json({ error: err.message }, 403));
|
|
85
|
+
|
|
86
|
+
const res = await app.request('/test');
|
|
87
|
+
expect(res.status).toBe(403);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('middleware allows authenticated user when org is enabled', async () => {
|
|
91
|
+
setOrganizationEnabled(true);
|
|
92
|
+
const app = createTestApp();
|
|
93
|
+
withUser(app, { id: '1', email: 'a@b.com', name: 'Test', role: 'admin' });
|
|
94
|
+
app.get('/test', requirePermission('invoices:write'), (c) => c.json({ ok: true }));
|
|
95
|
+
|
|
96
|
+
const res = await app.request('/test');
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('requireOrg', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
setOrganizationEnabled(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('throws descriptive error at call time when org is NOT enabled', () => {
|
|
107
|
+
setOrganizationEnabled(false);
|
|
108
|
+
expect(() => requireOrg()).toThrow(
|
|
109
|
+
'Organization plugin required. Enable organization in config.',
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('rejects user without active organization with 403 when org is enabled', async () => {
|
|
114
|
+
setOrganizationEnabled(true);
|
|
115
|
+
const app = createTestApp();
|
|
116
|
+
withUser(app, { id: '1', email: 'a@b.com', name: 'Test', role: 'user' });
|
|
117
|
+
app.get('/test', requireOrg(), (c) => c.json({ ok: true }));
|
|
118
|
+
app.onError((err, c) => c.json({ error: err.message }, 403));
|
|
119
|
+
|
|
120
|
+
const res = await app.request('/test');
|
|
121
|
+
expect(res.status).toBe(403);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('allows user with active organization when org is enabled', async () => {
|
|
125
|
+
setOrganizationEnabled(true);
|
|
126
|
+
const app = createTestApp();
|
|
127
|
+
withUser(app, { id: '1', email: 'a@b.com', name: 'Test', role: 'user', activeOrganizationId: 'org-1' });
|
|
128
|
+
app.get('/test', requireOrg(), (c) => c.json({ ok: true }));
|
|
129
|
+
|
|
130
|
+
const res = await app.request('/test');
|
|
131
|
+
expect(res.status).toBe(200);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('getActiveSchemas', () => {
|
|
136
|
+
it('always includes apikey schema', async () => {
|
|
137
|
+
const { getActiveSchemas } = await import('../schemas');
|
|
138
|
+
const schemas = getActiveSchemas();
|
|
139
|
+
expect(schemas.apikey).toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('excludes org schema by default', async () => {
|
|
143
|
+
const { getActiveSchemas } = await import('../schemas');
|
|
144
|
+
const schemas = getActiveSchemas();
|
|
145
|
+
expect(schemas.organization).toBeUndefined();
|
|
146
|
+
expect(schemas.member).toBeUndefined();
|
|
147
|
+
expect(schemas.invitation).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('includes org schema when organization is true', async () => {
|
|
151
|
+
const { getActiveSchemas } = await import('../schemas');
|
|
152
|
+
const schemas = getActiveSchemas({ organization: true });
|
|
153
|
+
expect(schemas.organization).toBeDefined();
|
|
154
|
+
expect(schemas.member).toBeDefined();
|
|
155
|
+
expect(schemas.invitation).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { hc } from 'hono/client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hono RPC Type Inference Validation (T17)
|
|
7
|
+
*
|
|
8
|
+
* FINDINGS:
|
|
9
|
+
* - Chained .route(): WORKS - runtime mounting works, and `typeof chainedApp` preserves route literals for RPC typing.
|
|
10
|
+
* - Dynamic reduce: DOESN'T WORK (for RPC typing) - runtime mounting works, but `new Hono() as Hono` widens the schema and drops typed client paths.
|
|
11
|
+
* - hc<AppType> inference: WORKS with chained route exports - typed client methods (`$get`, `$post`, `$url`) are inferred from mounted paths.
|
|
12
|
+
* - TypeScript compilation: passes with 2 chained routes when the dynamic reduce limitation is captured via `@ts-expect-error`.
|
|
13
|
+
*
|
|
14
|
+
* RECOMMENDATION for vobase:
|
|
15
|
+
* - Export an RPC-facing app type from chained `.route()` composition and avoid reduce-based widened `Hono` values for frontend RPC typing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type Invoice = { id: string; total: number };
|
|
19
|
+
|
|
20
|
+
const invoicingRouter = new Hono()
|
|
21
|
+
.get('/list', (c) => c.json({ invoices: [] as Invoice[] }))
|
|
22
|
+
.post('/create', (c) => c.json({ id: 'inv-001', total: 0 } as Invoice));
|
|
23
|
+
|
|
24
|
+
const ordersRouter = new Hono().get('/list', (c) =>
|
|
25
|
+
c.json({ orders: [] as Array<{ id: string }> }),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const chainedApp = new Hono()
|
|
29
|
+
.route('/api/invoicing', invoicingRouter)
|
|
30
|
+
.route('/api/orders', ordersRouter);
|
|
31
|
+
export type ChainedAppType = typeof chainedApp;
|
|
32
|
+
|
|
33
|
+
const chainedClient = hc<ChainedAppType>('http://localhost:3000');
|
|
34
|
+
|
|
35
|
+
const modules = [
|
|
36
|
+
{ name: 'invoicing', routes: invoicingRouter },
|
|
37
|
+
{ name: 'orders', routes: ordersRouter },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const dynamicApp = modules.reduce(
|
|
41
|
+
(acc, mod) => acc.route(`/api/${mod.name}`, mod.routes),
|
|
42
|
+
new Hono() as Hono,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
type DynamicAppType = typeof dynamicApp;
|
|
46
|
+
const dynamicClient = hc<DynamicAppType>('http://localhost:3000');
|
|
47
|
+
|
|
48
|
+
// biome-ignore lint/complexity/noBannedTypes: intentional Function check for type-level test
|
|
49
|
+
type IsFunction<T> = T extends Function ? true : false;
|
|
50
|
+
type Expect<T extends true> = T;
|
|
51
|
+
|
|
52
|
+
const chainedListGet = chainedClient.api.invoicing.list.$get;
|
|
53
|
+
const chainedCreatePost = chainedClient.api.invoicing.create.$post;
|
|
54
|
+
type _ChainedListGetCallable = Expect<IsFunction<typeof chainedListGet>>;
|
|
55
|
+
type _ChainedCreatePostCallable = Expect<IsFunction<typeof chainedCreatePost>>;
|
|
56
|
+
|
|
57
|
+
// @ts-expect-error Dynamic reduce app type loses literal route inference for RPC client access.
|
|
58
|
+
dynamicClient.api.invoicing.list.$get;
|
|
59
|
+
|
|
60
|
+
describe('Hono RPC type inference', () => {
|
|
61
|
+
it('mounts routes correctly via chained .route()', async () => {
|
|
62
|
+
const res = await chainedApp.request('http://localhost/api/invoicing/list');
|
|
63
|
+
|
|
64
|
+
expect(res.status).toBe(200);
|
|
65
|
+
const data = (await res.json()) as { invoices: Invoice[] };
|
|
66
|
+
expect(data).toHaveProperty('invoices');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('exposes typed hc<AppType> methods for chained routes', () => {
|
|
70
|
+
const listUrl = chainedClient.api.invoicing.list.$url();
|
|
71
|
+
const createUrl = chainedClient.api.invoicing.create.$url();
|
|
72
|
+
|
|
73
|
+
expect(listUrl.pathname).toBe('/api/invoicing/list');
|
|
74
|
+
expect(createUrl.pathname).toBe('/api/invoicing/create');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('mounts routes correctly via reduce() pattern used by createApp', async () => {
|
|
78
|
+
const invoicingRes = await dynamicApp.request(
|
|
79
|
+
'http://localhost/api/invoicing/list',
|
|
80
|
+
);
|
|
81
|
+
expect(invoicingRes.status).toBe(200);
|
|
82
|
+
|
|
83
|
+
const ordersRes = await dynamicApp.request(
|
|
84
|
+
'http://localhost/api/orders/list',
|
|
85
|
+
);
|
|
86
|
+
expect(ordersRes.status).toBe(200);
|
|
87
|
+
const ordersData = (await ordersRes.json()) as {
|
|
88
|
+
orders: Array<{ id: string }>;
|
|
89
|
+
};
|
|
90
|
+
expect(ordersData).toHaveProperty('orders');
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/app.test.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { afterAll, describe, expect, it } from 'bun:test';
|
|
3
|
+
import { shutdownManager } from 'bunqueue/client';
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
|
|
6
|
+
import { createApp } from './app';
|
|
7
|
+
import { notFound } from './infra/errors';
|
|
8
|
+
import type { VobaseModule } from './module';
|
|
9
|
+
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
shutdownManager();
|
|
12
|
+
rmSync('./data/bunqueue.db', { force: true });
|
|
13
|
+
rmSync('./data/bunqueue.db-shm', { force: true });
|
|
14
|
+
rmSync('./data/bunqueue.db-wal', { force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function makeModule(
|
|
18
|
+
name: string,
|
|
19
|
+
routeFactory: (routes: Hono) => void,
|
|
20
|
+
): VobaseModule {
|
|
21
|
+
const routes = new Hono();
|
|
22
|
+
routeFactory(routes);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name,
|
|
26
|
+
schema: { test: {} },
|
|
27
|
+
routes,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('createApp()', () => {
|
|
32
|
+
it('creates a Hono app for in-memory database', async () => {
|
|
33
|
+
const app = await createApp({ modules: [], database: ':memory:' });
|
|
34
|
+
expect(app).toBeInstanceOf(Hono);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('serves GET /health with status and uptime', async () => {
|
|
38
|
+
const app = await createApp({ modules: [], database: ':memory:' });
|
|
39
|
+
|
|
40
|
+
const response = await app.request('http://localhost/health');
|
|
41
|
+
const body = (await response.json()) as { status: string; uptime: number };
|
|
42
|
+
|
|
43
|
+
expect(response.status).toBe(200);
|
|
44
|
+
expect(body.status).toBe('ok');
|
|
45
|
+
expect(typeof body.uptime).toBe('number');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('mounts /api/auth/* routes (not 404)', async () => {
|
|
49
|
+
const app = await createApp({ modules: [], database: ':memory:' });
|
|
50
|
+
|
|
51
|
+
const response = await app.request('http://localhost/api/auth/get-session');
|
|
52
|
+
|
|
53
|
+
expect(response.status).not.toBe(404);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('mounts module routes under /api/{module}', async () => {
|
|
57
|
+
const testModule = makeModule('testmod', (routes) => {
|
|
58
|
+
routes.get('/ping', (c) => c.json({ pong: true }));
|
|
59
|
+
});
|
|
60
|
+
const app = await createApp({ modules: [testModule], database: ':memory:' });
|
|
61
|
+
|
|
62
|
+
const response = await app.request('http://localhost/api/testmod/ping');
|
|
63
|
+
const body = (await response.json()) as { pong: boolean };
|
|
64
|
+
|
|
65
|
+
expect(response.status).toBe(200);
|
|
66
|
+
expect(body.pong).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('uses global error handler for thrown VobaseError', async () => {
|
|
70
|
+
const throwingModule = makeModule('errors', (routes) => {
|
|
71
|
+
routes.get('/boom', () => {
|
|
72
|
+
throw notFound('Record');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
const app = await createApp({ modules: [throwingModule], database: ':memory:' });
|
|
76
|
+
|
|
77
|
+
const response = await app.request('http://localhost/api/errors/boom');
|
|
78
|
+
const body = (await response.json()) as {
|
|
79
|
+
error: { code: string; message: string; details?: unknown };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
expect(response.status).toBe(404);
|
|
83
|
+
expect(body.error.code).toBe('NOT_FOUND');
|
|
84
|
+
expect(body.error.message).toBe('Record not found');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('allows unauthenticated access on /api/* with optional session middleware', async () => {
|
|
88
|
+
const openModule = makeModule('open', (routes) => {
|
|
89
|
+
routes.get('/status', (c) => c.json({ user: c.get('user') }));
|
|
90
|
+
});
|
|
91
|
+
const app = await createApp({ modules: [openModule], database: ':memory:' });
|
|
92
|
+
|
|
93
|
+
const response = await app.request('http://localhost/api/open/status');
|
|
94
|
+
const body = (await response.json()) as { user: unknown };
|
|
95
|
+
|
|
96
|
+
expect(response.status).toBe(200);
|
|
97
|
+
expect(body.user).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
});
|