devlyn-cli 0.3.0 → 0.4.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.
@@ -0,0 +1,241 @@
1
+ # Test Infrastructure Reference
2
+
3
+ Test setup, seed factories, and integration test patterns for Better Auth with Bun.
4
+
5
+ ## Table of Contents
6
+ 1. [Test Preload](#test-preload)
7
+ 2. [Seed Data Factory](#seed-data-factory)
8
+ 3. [Integration Test App](#integration-test-app)
9
+ 4. [Database Cleanup](#database-cleanup)
10
+ 5. [Key Testing Patterns](#key-testing-patterns)
11
+
12
+ ## Test Preload
13
+
14
+ Prevents the most common testing pitfall: real emails sent during test runs.
15
+
16
+ ```typescript
17
+ // src/test-utils/setup.ts — preloaded via bunfig.toml
18
+ // Without this, test signups send real emails through Resend
19
+ // when RESEND_API_KEY is in your .env file.
20
+ process.env.NODE_ENV = "test";
21
+ process.env.RESEND_API_KEY = ""; // Force email to console-log fallback
22
+
23
+ // Bridge test database — use a separate DB for tests
24
+ if (process.env.TEST_DATABASE_URL) {
25
+ process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
26
+ }
27
+ ```
28
+
29
+ ```toml
30
+ # bunfig.toml
31
+ [test]
32
+ preload = ["./src/test-utils/setup.ts"]
33
+ ```
34
+
35
+ **Why preload?** Bun's preload runs before any test file imports. This guarantees that config validation (which runs at import time) sees the correct environment variables. Setting `RESEND_API_KEY = ""` before any module loads ensures the email module's lazy client never initializes.
36
+
37
+ ## Seed Data Factory
38
+
39
+ Creates a complete tenant hierarchy for integration tests. Every call produces unique identifiers to prevent collisions in parallel test runs.
40
+
41
+ ```typescript
42
+ // src/test-utils/db.ts
43
+ import { db } from "../db";
44
+ import {
45
+ organizations, users, orgMemberships,
46
+ projects, apiKeys, sessions, accounts,
47
+ verifications, invitations,
48
+ } from "../db/schema";
49
+
50
+ export async function seedTestData() {
51
+ const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
52
+
53
+ // Create org → user → membership → project → API key
54
+ const [org] = await db.insert(organizations).values({
55
+ name: `Test Org ${uniqueSuffix}`,
56
+ slug: `test-org-${uniqueSuffix}`,
57
+ plan: "free",
58
+ }).returning();
59
+
60
+ const [user] = await db.insert(users).values({
61
+ email: `test-${uniqueSuffix}@example.com`,
62
+ name: "Test User",
63
+ emailVerified: true,
64
+ }).returning();
65
+
66
+ await db.insert(orgMemberships).values({
67
+ organizationId: org.id,
68
+ userId: user.id,
69
+ role: "owner",
70
+ });
71
+
72
+ const [project] = await db.insert(projects).values({
73
+ organizationId: org.id,
74
+ name: `Test Project ${uniqueSuffix}`,
75
+ slug: `test-project-${uniqueSuffix}`,
76
+ }).returning();
77
+
78
+ // Generate API key
79
+ const { key, hash, prefix } = await generateTestApiKey();
80
+ const [apiKey] = await db.insert(apiKeys).values({
81
+ projectId: project.id,
82
+ name: "Test Key",
83
+ keyHash: hash,
84
+ keyPrefix: prefix,
85
+ }).returning();
86
+
87
+ return { org, user, project, apiKey, plaintextKey: key };
88
+ }
89
+
90
+ // Helper: generate a test API key
91
+ async function generateTestApiKey() {
92
+ const { API_KEY_PREFIX, base62Encode, hashKey } = await import("../lib/api-keys");
93
+ const randomBytes = crypto.getRandomValues(new Uint8Array(32));
94
+ const key = API_KEY_PREFIX + base62Encode(randomBytes);
95
+ const hash = await hashKey(key);
96
+ const prefix = key.slice(0, 12);
97
+ return { key, hash, prefix };
98
+ }
99
+ ```
100
+
101
+ **Key design decisions:**
102
+ - `uniqueSuffix` uses `Date.now()` + random chars for collision-free parallel tests
103
+ - `emailVerified: true` so tests don't need to go through email verification flow
104
+ - Returns `plaintextKey` so tests can immediately make authenticated requests
105
+
106
+ ## Integration Test App
107
+
108
+ Builds the full middleware chain matching production. This catches middleware ordering bugs before they reach production.
109
+
110
+ ```typescript
111
+ // src/test-utils/app.ts
112
+ import { Hono } from "hono";
113
+
114
+ // Lazy imports to avoid circular dependency issues during test setup
115
+ export function createIntegrationApp() {
116
+ const { authMiddleware } = require("../middleware/auth");
117
+ const { tenantContextMiddleware } = require("../middleware/tenant-context");
118
+ const { rateLimitMiddleware } = require("../middleware/rate-limit");
119
+
120
+ const app = new Hono();
121
+
122
+ // Request ID
123
+ app.use("*", async (c, next) => {
124
+ c.set("requestId", crypto.randomUUID());
125
+ await next();
126
+ });
127
+
128
+ // Auth → Tenant Context → Rate Limit (same order as production)
129
+ app.use("*", authMiddleware);
130
+ app.use("*", tenantContextMiddleware);
131
+ app.use("*", rateLimitMiddleware);
132
+
133
+ // Mount route handlers here...
134
+ return app;
135
+ }
136
+ ```
137
+
138
+ **Why lazy imports?** The auth module imports the database module, which reads `DATABASE_URL` from the environment. If imported eagerly at the top level, the test preload script might not have run yet, causing config validation to fail.
139
+
140
+ ## Database Cleanup
141
+
142
+ Delete tables in FK dependency order to avoid constraint violations.
143
+
144
+ ```typescript
145
+ // Clean tables in FK dependency order
146
+ export async function cleanupDatabase() {
147
+ // Children first, parents last
148
+ await db.delete(apiKeys);
149
+ await db.delete(projects);
150
+ await db.delete(orgMemberships);
151
+ await db.delete(sessions);
152
+ await db.delete(accounts);
153
+ await db.delete(verifications);
154
+ await db.delete(invitations);
155
+ await db.delete(organizations);
156
+ await db.delete(users);
157
+ }
158
+ ```
159
+
160
+ **Order matters.** If you try to delete `organizations` before `projects`, the FK constraint on `projects.organization_id` will reject the delete. Start from leaf tables and work toward root tables.
161
+
162
+ ## Key Testing Patterns
163
+
164
+ ### Test Auth via API Key
165
+
166
+ ```typescript
167
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
168
+
169
+ describe("Protected endpoint", () => {
170
+ let testData: Awaited<ReturnType<typeof seedTestData>>;
171
+
172
+ beforeAll(async () => {
173
+ testData = await seedTestData();
174
+ });
175
+
176
+ afterAll(async () => {
177
+ await cleanupDatabase();
178
+ });
179
+
180
+ test("returns 200 with valid API key", async () => {
181
+ const app = createIntegrationApp();
182
+ const res = await app.request("/v1/endpoint", {
183
+ headers: {
184
+ Authorization: `Bearer ${testData.plaintextKey}`,
185
+ },
186
+ });
187
+ expect(res.status).toBe(200);
188
+ });
189
+
190
+ test("returns 401 without auth", async () => {
191
+ const app = createIntegrationApp();
192
+ const res = await app.request("/v1/endpoint");
193
+ expect(res.status).toBe(401);
194
+ });
195
+ });
196
+ ```
197
+
198
+ ### Test Signup Flow
199
+
200
+ ```typescript
201
+ test("signup creates user and personal org", async () => {
202
+ const res = await app.request("/auth/sign-up/email", {
203
+ method: "POST",
204
+ headers: { "Content-Type": "application/json" },
205
+ body: JSON.stringify({
206
+ email: `signup-test-${Date.now()}@example.com`,
207
+ password: "secure-password-123",
208
+ name: "Test User",
209
+ }),
210
+ });
211
+
212
+ expect(res.status).toBe(200);
213
+
214
+ // Verify auto-org creation via databaseHooks
215
+ const user = await db.select().from(users)
216
+ .where(eq(users.email, email)).limit(1);
217
+
218
+ const membership = await db.select().from(orgMemberships)
219
+ .where(eq(orgMemberships.userId, user[0].id)).limit(1);
220
+
221
+ expect(membership).toHaveLength(1);
222
+ expect(membership[0].role).toBe("owner");
223
+ });
224
+ ```
225
+
226
+ ### Test Tenant Isolation
227
+
228
+ ```typescript
229
+ test("cannot access resources from another org", async () => {
230
+ const org1 = await seedTestData();
231
+ const org2 = await seedTestData();
232
+
233
+ // Use org1's API key to try accessing org2's project
234
+ const res = await app.request(`/v1/projects/${org2.project.id}`, {
235
+ headers: { Authorization: `Bearer ${org1.plaintextKey}` },
236
+ });
237
+
238
+ // Should be 404 (not 403, to prevent ID enumeration)
239
+ expect(res.status).toBe(404);
240
+ });
241
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code configuration toolkit for teams",
5
5
  "bin": {
6
6
  "devlyn": "bin/devlyn.js"