@crossdelta/platform-sdk 0.3.41 → 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.
package/README.md
CHANGED
|
@@ -30,36 +30,116 @@
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## 🎯 Why This Exists
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
- You prefer **monorepos + Infrastructure-as-Code** as your architectural baseline
|
|
37
|
-
- You're okay with **DigitalOcean as the initial cloud provider** (AWS/GCP coming soon)
|
|
35
|
+
Building microservice platforms is hard. You start with great intentions, but complexity creeps in fast:
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
- **Scattered repositories** with inconsistent conventions across services
|
|
38
|
+
- **No unified DX** — every team member invents their own scripts
|
|
39
|
+
- **Infrastructure drift** — Terraform/Pulumi configs live in separate repos
|
|
40
|
+
- **Environment hell** — `.env` files manually maintained, ports collide, services don't talk
|
|
41
|
+
- **Slow onboarding** — new developers spend days setting up the platform
|
|
42
|
+
|
|
43
|
+
**`pf` acts like an opinionated platform engineer on your team.** It's a single CLI that:
|
|
44
|
+
|
|
45
|
+
- Scaffolds production-ready monorepos with best practices baked in
|
|
46
|
+
- Generates microservices (manually or with AI) with consistent structure
|
|
47
|
+
- Auto-wires infrastructure, environment variables, and port assignments
|
|
48
|
+
- Unifies your dev workflow: one command to run everything locally
|
|
49
|
+
|
|
50
|
+
Built for teams using **Turborepo + Pulumi + NATS + Bun** who want to move fast without sacrificing quality.
|
|
44
51
|
|
|
45
52
|
<br />
|
|
46
53
|
|
|
47
54
|
---
|
|
48
55
|
|
|
49
|
-
##
|
|
56
|
+
## 🧩 What This SDK Is / Is Not
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
### ✅ `pf` is:
|
|
59
|
+
|
|
60
|
+
- **Opinionated CLI** for event-driven microservice platforms
|
|
61
|
+
- **Turborepo scaffolder** with Pulumi IaC and NATS messaging built-in
|
|
62
|
+
- **AI-powered code generator** that creates complete services from natural language
|
|
63
|
+
- **Unified dev workflow** — one command to rule them all (`bun dev`)
|
|
64
|
+
- **DigitalOcean-first** deployment target (App Platform + DOKS)
|
|
65
|
+
|
|
66
|
+
### ❌ `pf` is not:
|
|
67
|
+
|
|
68
|
+
- **Not a generic microservice generator** — we make architectural choices for you
|
|
69
|
+
- **Not cloud-agnostic** (yet) — DigitalOcean first, AWS/GCP coming later
|
|
70
|
+
- **Not a runtime manager** — we scaffold code, you deploy with Pulumi
|
|
71
|
+
- **Not a replacement for Kubernetes** — we generate K8s configs via Pulumi
|
|
72
|
+
|
|
73
|
+
<br />
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🏗️ Architecture at 10,000 ft
|
|
78
|
+
|
|
79
|
+
When you create a workspace with `pf`, you get a **Turborepo monorepo** with this structure:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
my-platform/
|
|
83
|
+
├── services/ # Microservices (Hono, NestJS)
|
|
84
|
+
│ ├── orders/ # Example: Order processing service
|
|
85
|
+
│ ├── notifications/# Example: Notification service
|
|
86
|
+
│ └── nats/ # NATS message broker (auto-scaffolded)
|
|
87
|
+
├── apps/ # Frontend apps (optional: Qwik, Next.js, etc.)
|
|
88
|
+
├── packages/ # Shared libraries
|
|
89
|
+
│ ├── cloudevents/ # Event publishing/consuming toolkit
|
|
90
|
+
│ └── telemetry/ # OpenTelemetry setup
|
|
91
|
+
├── infra/ # Pulumi Infrastructure-as-Code
|
|
92
|
+
│ ├── index.ts # Main Pulumi program
|
|
93
|
+
│ └── services/ # Per-service K8s configs
|
|
94
|
+
│ ├── orders.ts
|
|
95
|
+
│ └── notifications.ts
|
|
96
|
+
├── .github/
|
|
97
|
+
│ └── workflows/ # CI/CD pipelines (auto-generated)
|
|
98
|
+
├── turbo.json # Turborepo task orchestration
|
|
99
|
+
└── .env.local # Auto-generated from infra configs
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Key Architectural Decisions
|
|
103
|
+
|
|
104
|
+
1. **NATS + JetStream baseline** — Event-driven communication is built-in, not bolted on
|
|
105
|
+
2. **Infrastructure-as-Code by default** — Every service has a matching `infra/services/<name>.ts` config
|
|
106
|
+
3. **Auto-wiring everywhere** — Ports, env vars, and NATS subjects are derived automatically
|
|
107
|
+
4. **Opinionated conventions** — Event handlers live in `src/handlers/*.event.ts`, business logic in `src/use-cases/*.use-case.ts`
|
|
108
|
+
5. **Bun-first DX** — Ultra-fast installs, tests, and dev server with fallback to npm/yarn
|
|
109
|
+
|
|
110
|
+
### Event-Driven Mental Model
|
|
111
|
+
|
|
112
|
+
Services communicate via **CloudEvents** over **NATS JetStream**:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Service A publishes an event
|
|
116
|
+
await publish('orders.created', { orderId: '123', total: 99.99 })
|
|
117
|
+
|
|
118
|
+
// Service B auto-discovers and handles it
|
|
119
|
+
// File: services/notifications/src/handlers/order-created.event.ts
|
|
120
|
+
export default handleEvent(
|
|
121
|
+
{ schema: OrderCreatedSchema, type: 'orders.created' },
|
|
122
|
+
async (data) => { await sendNotification(data) }
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
No manual NATS subscriptions. No boilerplate. Just **convention over configuration**.
|
|
127
|
+
|
|
128
|
+
<br />
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🧭 Design Principles
|
|
133
|
+
|
|
134
|
+
These principles guide every decision in `pf`:
|
|
135
|
+
|
|
136
|
+
- **🥟 Bun-first DX** — Leverage Bun's speed for installs, tests, and dev mode (npm/yarn fallback supported)
|
|
137
|
+
- **📦 Monorepo-centric** — One repo to rule them all. Turborepo for caching and parallel builds
|
|
138
|
+
- **🏗️ Infrastructure-as-Code by default** — No ClickOps. Every service has Pulumi config
|
|
139
|
+
- **🔁 Event-driven communication baked in** — NATS + JetStream are first-class citizens, not afterthoughts
|
|
140
|
+
- **🌊 DigitalOcean-first, cloud-agnostic later** — Start simple with DO, expand to AWS/GCP when needed
|
|
141
|
+
- **🤖 AI-augmented development** — Use AI to generate complete services, not just snippets
|
|
142
|
+
- **📏 Convention over configuration** — Strong opinions enable automation
|
|
63
143
|
|
|
64
144
|
<br />
|
|
65
145
|
|
|
@@ -126,30 +206,230 @@ This will:
|
|
|
126
206
|
- 🔌 Auto-assign unique ports per service
|
|
127
207
|
- 📦 Monitor for file changes and hot-reload
|
|
128
208
|
|
|
209
|
+
### 4. Essential workspace commands
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Run all tests across the monorepo
|
|
213
|
+
bun test
|
|
214
|
+
|
|
215
|
+
# Lint and format all code with Biome
|
|
216
|
+
bun lint
|
|
217
|
+
|
|
218
|
+
# Build all packages and services
|
|
219
|
+
bun run build
|
|
220
|
+
```
|
|
221
|
+
|
|
129
222
|
<br />
|
|
130
223
|
|
|
131
224
|
---
|
|
132
225
|
|
|
133
|
-
##
|
|
226
|
+
## 📘 Typical Workflows
|
|
227
|
+
|
|
228
|
+
Here's how developers actually use `pf` in real-world scenarios:
|
|
134
229
|
|
|
230
|
+
### Workflow 1: Start a New Platform
|
|
231
|
+
|
|
232
|
+
You're building a new product from scratch and want to establish a solid foundation.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# Step 1: Scaffold the workspace
|
|
236
|
+
bunx @crossdelta/platform-sdk new workspace orderboss-platform \
|
|
237
|
+
--github-owner orderboss \
|
|
238
|
+
--pulumi-stack dev \
|
|
239
|
+
-y
|
|
240
|
+
|
|
241
|
+
cd orderboss-platform
|
|
242
|
+
|
|
243
|
+
# Step 2: Configure infrastructure (optional)
|
|
244
|
+
# Edit infra/config.ts to customize:
|
|
245
|
+
# - DigitalOcean region (defaults to nyc3)
|
|
246
|
+
# - Kubernetes cluster size
|
|
247
|
+
# - Database instance sizes
|
|
248
|
+
|
|
249
|
+
# Step 3: Start local development
|
|
250
|
+
bun dev
|
|
135
251
|
```
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
252
|
+
|
|
253
|
+
**What you get:**
|
|
254
|
+
- ✅ Turborepo monorepo with Biome linting/formatting
|
|
255
|
+
- ✅ NATS service running in Docker
|
|
256
|
+
- ✅ Pulumi infrastructure setup for DigitalOcean
|
|
257
|
+
- ✅ GitHub Actions workflows for CI/CD
|
|
258
|
+
- ✅ `.env.local` auto-generated with all service ports
|
|
259
|
+
|
|
260
|
+
**Time to first service running:** ~60 seconds
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### Workflow 2: Add a New Microservice
|
|
265
|
+
|
|
266
|
+
Your platform is growing and you need to add a new service for handling payments.
|
|
267
|
+
|
|
268
|
+
**Option A: Manual scaffolding (fast, predictable)**
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# Generate a lightweight Hono microservice
|
|
272
|
+
pf new hono-micro services/payments -y
|
|
273
|
+
|
|
274
|
+
# What gets auto-generated:
|
|
275
|
+
# ✅ services/payments/src/index.ts (Hono server)
|
|
276
|
+
# ✅ services/payments/src/handlers/*.event.ts (example event handler)
|
|
277
|
+
# ✅ infra/services/payments.ts (Pulumi K8s config)
|
|
278
|
+
# ✅ services/payments/README.md (service documentation)
|
|
279
|
+
# ✅ Dockerfile + package.json with scripts
|
|
280
|
+
# ✅ Auto-assigned port (e.g., 4003)
|
|
281
|
+
|
|
282
|
+
# Start the service
|
|
283
|
+
bun dev
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Option B: AI-powered generation (intelligent, complete)**
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# First time: configure AI provider
|
|
290
|
+
pf setup --ai
|
|
291
|
+
|
|
292
|
+
# Generate a complete service with AI
|
|
293
|
+
pf new hono-micro services/payments --ai \
|
|
294
|
+
-d "Stripe payment processing: handle checkout.session.completed webhooks, \
|
|
295
|
+
publish payment.succeeded events, update order status in database"
|
|
296
|
+
|
|
297
|
+
# AI generates:
|
|
298
|
+
# ✅ Complete service implementation with Stripe SDK
|
|
299
|
+
# ✅ Event handlers for incoming orders
|
|
300
|
+
# ✅ Event publishers for payment lifecycle
|
|
301
|
+
# ✅ Use cases with validation logic
|
|
302
|
+
# ✅ Test files for all use cases
|
|
303
|
+
# ✅ Environment variable documentation
|
|
304
|
+
# ✅ Complete README with API docs
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**When to use which:**
|
|
308
|
+
- **Manual scaffolding:** You know exactly what you're building, want full control
|
|
309
|
+
- **AI generation:** Bootstrapping new domains, exploring integrations, speeding up prototyping
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### Workflow 3: Build an Event-Driven Feature
|
|
314
|
+
|
|
315
|
+
You want to send notifications whenever a new order is created.
|
|
316
|
+
|
|
317
|
+
**Step 1: Create the notification service**
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
pf new hono-micro services/notifications -y
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Step 2: Implement the event handler**
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// services/notifications/src/handlers/order-created.event.ts
|
|
327
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
328
|
+
import { z } from 'zod'
|
|
329
|
+
import { sendNotification } from '../use-cases/send-notification.use-case'
|
|
330
|
+
|
|
331
|
+
const OrderCreatedSchema = z.object({
|
|
332
|
+
orderId: z.string(),
|
|
333
|
+
customerId: z.string(),
|
|
334
|
+
total: z.number(),
|
|
335
|
+
items: z.array(
|
|
336
|
+
z.object({
|
|
337
|
+
productId: z.string(),
|
|
338
|
+
quantity: z.number(),
|
|
339
|
+
price: z.number(),
|
|
340
|
+
}),
|
|
341
|
+
).optional(),
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// Export type for use in use-cases
|
|
345
|
+
export type OrderCreatedEvent = z.infer<typeof OrderCreatedSchema>
|
|
346
|
+
|
|
347
|
+
export default handleEvent(
|
|
348
|
+
{
|
|
349
|
+
schema: OrderCreatedSchema,
|
|
350
|
+
type: 'orders.created', // Event type to subscribe to
|
|
351
|
+
},
|
|
352
|
+
async (data) => {
|
|
353
|
+
await sendNotification(data)
|
|
354
|
+
},
|
|
355
|
+
)
|
|
151
356
|
```
|
|
152
357
|
|
|
358
|
+
**Step 3: Implement the use case**
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// services/notifications/src/use-cases/send-notification.use-case.ts
|
|
362
|
+
import type { OrderCreatedEvent } from '../handlers/order-created.event'
|
|
363
|
+
|
|
364
|
+
export async function sendNotification(data: OrderCreatedEvent): Promise<void> {
|
|
365
|
+
// Full type inference from the event handler schema
|
|
366
|
+
console.log(`Sending notification for order: ${data.orderId}`)
|
|
367
|
+
|
|
368
|
+
// Your notification logic here:
|
|
369
|
+
// - Send email via SendGrid
|
|
370
|
+
// - Push notification via Firebase
|
|
371
|
+
// - SMS via Twilio
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Step 4: Start consuming events**
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// services/notifications/src/index.ts
|
|
379
|
+
import '@crossdelta/telemetry' // Must be first import
|
|
380
|
+
|
|
381
|
+
import { consumeJetStreamEvents } from '@crossdelta/cloudevents/transports/nats'
|
|
382
|
+
import { Hono } from 'hono'
|
|
383
|
+
|
|
384
|
+
const port = Number(process.env.PORT || process.env.NOTIFICATIONS_PORT) || 4002
|
|
385
|
+
const app = new Hono()
|
|
386
|
+
|
|
387
|
+
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
388
|
+
|
|
389
|
+
// Auto-discover and register all event handlers
|
|
390
|
+
consumeJetStreamEvents({
|
|
391
|
+
stream: 'ORDERS',
|
|
392
|
+
subjects: ['orders.>'], // Subscribe to all orders.* events
|
|
393
|
+
consumer: 'notifications',
|
|
394
|
+
discover: './src/handlers/**/*.event.ts',
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
Bun.serve({ port, fetch: app.fetch })
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Step 5: Publish events from the orders service**
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// services/orders/src/routes/create-order.ts
|
|
404
|
+
import { publish } from '@crossdelta/cloudevents'
|
|
405
|
+
|
|
406
|
+
export async function createOrder(orderData) {
|
|
407
|
+
const order = await db.orders.create(orderData)
|
|
408
|
+
|
|
409
|
+
// Publish event to NATS
|
|
410
|
+
await publish('orders.created', {
|
|
411
|
+
orderId: order.id,
|
|
412
|
+
customerId: order.customerId,
|
|
413
|
+
total: order.total,
|
|
414
|
+
items: order.items,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
return order
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**How it works:**
|
|
422
|
+
1. **Publishing:** `publish()` sends a CloudEvent to NATS JetStream
|
|
423
|
+
2. **Auto-discovery:** `consumeJetStreamEvents()` scans `src/handlers/*.event.ts` and registers handlers
|
|
424
|
+
3. **Type safety:** Zod schema validates incoming events, provides TypeScript types
|
|
425
|
+
4. **Decoupling:** Services don't know about each other, only about events
|
|
426
|
+
|
|
427
|
+
**Benefits:**
|
|
428
|
+
- ✅ Zero boilerplate for NATS subscriptions
|
|
429
|
+
- ✅ Type-safe event handling with Zod
|
|
430
|
+
- ✅ Automatic retries and error handling (JetStream guarantees)
|
|
431
|
+
- ✅ Easy to add new consumers without touching existing services
|
|
432
|
+
|
|
153
433
|
<br />
|
|
154
434
|
|
|
155
435
|
---
|
|
@@ -291,16 +571,38 @@ export const config: K8sServiceConfig = {
|
|
|
291
571
|
|
|
292
572
|
## 🚢 Deployment
|
|
293
573
|
|
|
574
|
+
### Local Deployment
|
|
575
|
+
|
|
294
576
|
```bash
|
|
295
577
|
pulumi login
|
|
296
578
|
pulumi up --stack dev
|
|
297
579
|
```
|
|
298
580
|
|
|
299
|
-
GitHub
|
|
581
|
+
### CI/CD with GitHub Actions
|
|
582
|
+
|
|
583
|
+
Every workspace includes pre-configured GitHub Actions workflows in `.github/workflows/`:
|
|
584
|
+
|
|
585
|
+
| Workflow | Trigger | Purpose |
|
|
586
|
+
|----------|---------|---------|
|
|
587
|
+
| **`lint-and-tests.yml`** | Pull Requests to `main` | Runs `bun lint` and `bun test` on PRs |
|
|
588
|
+
| **`build-and-deploy.yml`** | Push to `main` | Builds Docker images, pushes to GHCR, deploys via Pulumi |
|
|
589
|
+
| **`publish-packages.yml`** | Changes in `packages/` | Auto-publishes packages to npm with versioning |
|
|
590
|
+
|
|
591
|
+
### Required GitHub Secrets
|
|
592
|
+
|
|
593
|
+
Configure these secrets in your repository settings (`Settings` → `Secrets and variables` → `Actions`):
|
|
594
|
+
|
|
595
|
+
| Secret | Description | Required For |
|
|
596
|
+
|--------|-------------|--------------|
|
|
597
|
+
| `PULUMI_ACCESS_TOKEN` | Pulumi Cloud access token | All deployments |
|
|
598
|
+
| `DIGITALOCEAN_TOKEN` | DigitalOcean API token | Infrastructure provisioning |
|
|
599
|
+
| `NPM_TOKEN` | npm registry token | Package publishing (optional) |
|
|
600
|
+
| `GHCR_TOKEN` | GitHub Container Registry PAT | Docker image publishing |
|
|
300
601
|
|
|
301
|
-
|
|
302
|
-
-
|
|
303
|
-
-
|
|
602
|
+
**Get tokens:**
|
|
603
|
+
- **Pulumi:** https://app.pulumi.com/account/tokens
|
|
604
|
+
- **DigitalOcean:** https://cloud.digitalocean.com/account/api/tokens
|
|
605
|
+
- **npm:** https://www.npmjs.com/settings/~/tokens
|
|
304
606
|
|
|
305
607
|
<br />
|
|
306
608
|
|
|
@@ -339,38 +339,39 @@ describe('Service Health Check', () => {
|
|
|
339
339
|
|
|
340
340
|
#### `src/use-cases/business-logic.use-case.test.ts`
|
|
341
341
|
```typescript
|
|
342
|
-
import { describe, expect, it
|
|
342
|
+
import { describe, expect, it } from 'bun:test'
|
|
343
343
|
import { myUseCase } from './business-logic.use-case'
|
|
344
344
|
|
|
345
345
|
describe('BusinessLogic Use Case', () => {
|
|
346
|
-
beforeEach(() => {
|
|
347
|
-
vi.clearAllMocks()
|
|
348
|
-
})
|
|
349
|
-
|
|
350
346
|
it('should handle valid input correctly', async () => {
|
|
351
347
|
const result = await myUseCase({ id: '123' })
|
|
352
348
|
expect(result).toBeDefined()
|
|
353
349
|
})
|
|
354
350
|
|
|
355
351
|
it('should throw error for invalid input', async () => {
|
|
356
|
-
await expect(myUseCase({ id: '' })).rejects.toThrow()
|
|
352
|
+
await expect(myUseCase({ id: '' })).rejects.toThrow('Invalid input')
|
|
357
353
|
})
|
|
358
354
|
})
|
|
359
355
|
```
|
|
360
356
|
|
|
361
357
|
**Test Guidelines:**
|
|
358
|
+
- **Keep tests simple and lightweight** - Bun's test runner is minimal, not a full-featured test framework
|
|
362
359
|
- Write tests ONLY for use cases (NOT event handlers)
|
|
363
360
|
- Event handlers are thin wrappers - the use cases contain the testable logic
|
|
364
|
-
- Use Bun's native test runner: `import { describe, expect, it, beforeEach, afterEach
|
|
365
|
-
-
|
|
361
|
+
- Use Bun's native test runner: `import { describe, expect, it, beforeEach, afterEach } from 'bun:test'`
|
|
362
|
+
- **NO mocking available** - Bun's test runner does NOT have `vi`, `mock`, or similar utilities
|
|
363
|
+
- ❌ NO `vi` object (that's Vitest, not Bun)
|
|
364
|
+
- ❌ NO `jest.fn()` or `jest.mock()`
|
|
365
|
+
- ❌ NO module mocking
|
|
366
|
+
- ✅ Use simple, direct testing without mocks
|
|
366
367
|
- Use descriptive test names in English
|
|
367
368
|
- Test both happy paths and error cases
|
|
368
|
-
- Focus on validation and error handling
|
|
369
|
-
- DO NOT mock external libraries
|
|
369
|
+
- Focus on validation and error handling (input params, env vars)
|
|
370
|
+
- **DO NOT mock external libraries** - Bun doesn't support mocking, keep tests simple
|
|
370
371
|
- Test environment variable validation and input validation
|
|
371
|
-
-
|
|
372
|
+
- Keep cleanup simple in `afterEach`: restore `process.env` only
|
|
372
373
|
|
|
373
|
-
**Example:
|
|
374
|
+
**Example: Simple validation and error handling tests**
|
|
374
375
|
```typescript
|
|
375
376
|
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
|
376
377
|
import { sendNotification } from './send-notification.use-case'
|
|
@@ -379,10 +380,12 @@ describe('Send Notification Use Case', () => {
|
|
|
379
380
|
const originalEnv = process.env
|
|
380
381
|
|
|
381
382
|
beforeEach(() => {
|
|
383
|
+
// Save original environment
|
|
382
384
|
process.env = { ...originalEnv }
|
|
383
385
|
})
|
|
384
386
|
|
|
385
387
|
afterEach(() => {
|
|
388
|
+
// Restore original environment (no vi.resetModules needed)
|
|
386
389
|
process.env = originalEnv
|
|
387
390
|
})
|
|
388
391
|
|
|
@@ -402,6 +405,71 @@ describe('Send Notification Use Case', () => {
|
|
|
402
405
|
})
|
|
403
406
|
```
|
|
404
407
|
|
|
408
|
+
**What to test:**
|
|
409
|
+
- ✅ Input validation (missing/empty parameters)
|
|
410
|
+
- ✅ Environment variable validation
|
|
411
|
+
- ✅ Error messages and error types
|
|
412
|
+
- ✅ Basic logic flows
|
|
413
|
+
- ❌ Complex mocking scenarios
|
|
414
|
+
- ❌ External API integrations (Pusher, AWS, etc.)
|
|
415
|
+
- ❌ Module reloading/resetting
|
|
416
|
+
|
|
417
|
+
**Test Strategy:**
|
|
418
|
+
- Focus on **validation** (input parameters, environment variables)
|
|
419
|
+
- Test **error handling** and edge cases
|
|
420
|
+
- Keep tests **simple** - avoid complex mocking of external libraries
|
|
421
|
+
- Test the **business logic**, not the external API calls
|
|
422
|
+
- If a function calls an external API, test the validation BEFORE the call, not the call itself
|
|
423
|
+
|
|
424
|
+
**Example of what NOT to do:**
|
|
425
|
+
```typescript
|
|
426
|
+
// ❌ BAD - Trying to mock Pusher (not possible in Bun)
|
|
427
|
+
import { describe, expect, it, beforeEach, afterEach, vi } from 'bun:test'
|
|
428
|
+
|
|
429
|
+
describe('Bad Test', () => {
|
|
430
|
+
afterEach(() => {
|
|
431
|
+
vi.resetModules() // ❌ This doesn't exist in Bun!
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('should send notification', async () => {
|
|
435
|
+
// ❌ Trying to mock external library
|
|
436
|
+
const mockPusher = vi.fn() // ❌ vi doesn't exist!
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Example of what TO do:**
|
|
442
|
+
```typescript
|
|
443
|
+
// ✅ GOOD - Simple validation tests
|
|
444
|
+
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
|
445
|
+
|
|
446
|
+
describe('Good Test', () => {
|
|
447
|
+
const originalEnv = process.env
|
|
448
|
+
|
|
449
|
+
beforeEach(() => {
|
|
450
|
+
process.env = { ...originalEnv }
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
afterEach(() => {
|
|
454
|
+
process.env = originalEnv
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should throw error if orderId is missing', async () => {
|
|
458
|
+
await expect(
|
|
459
|
+
sendNotification({ orderId: '', customerId: 'cust-1' })
|
|
460
|
+
).rejects.toThrow('Missing orderId')
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('should throw error if credentials not set', async () => {
|
|
464
|
+
delete process.env.PUSHER_BEAMS_INSTANCE_ID
|
|
465
|
+
|
|
466
|
+
await expect(
|
|
467
|
+
sendNotification({ orderId: '123', customerId: 'cust-1' })
|
|
468
|
+
).rejects.toThrow('Missing Pusher Beams credentials')
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
```
|
|
472
|
+
|
|
405
473
|
### README
|
|
406
474
|
|
|
407
475
|
Generate a service-specific README documenting:
|