@ryuenn3123/agentic-senior-core 1.8.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/.agent-context/blueprints/api-nextjs.md +184 -0
- package/.agent-context/blueprints/aspnet-api.md +247 -0
- package/.agent-context/blueprints/ci-github-actions.md +226 -0
- package/.agent-context/blueprints/ci-gitlab.md +200 -0
- package/.agent-context/blueprints/fastapi-service.md +210 -0
- package/.agent-context/blueprints/go-service.md +217 -0
- package/.agent-context/blueprints/graphql-grpc-api.md +51 -0
- package/.agent-context/blueprints/infrastructure-as-code.md +62 -0
- package/.agent-context/blueprints/kubernetes-manifests.md +76 -0
- package/.agent-context/blueprints/laravel-api.md +223 -0
- package/.agent-context/blueprints/nestjs-logic.md +247 -0
- package/.agent-context/blueprints/observability.md +227 -0
- package/.agent-context/blueprints/spring-boot-api.md +218 -0
- package/.agent-context/policies/llm-judge-threshold.json +20 -0
- package/.agent-context/profiles/platform.md +13 -0
- package/.agent-context/profiles/regulated.md +13 -0
- package/.agent-context/profiles/startup.md +13 -0
- package/.agent-context/prompts/init-project.md +86 -0
- package/.agent-context/prompts/refactor.md +45 -0
- package/.agent-context/prompts/review-code.md +47 -0
- package/.agent-context/review-checklists/architecture-review.md +70 -0
- package/.agent-context/review-checklists/frontend-usability.md +33 -0
- package/.agent-context/review-checklists/performance-audit.md +65 -0
- package/.agent-context/review-checklists/pr-checklist.md +97 -0
- package/.agent-context/review-checklists/release-operations.md +29 -0
- package/.agent-context/review-checklists/security-audit.md +113 -0
- package/.agent-context/rules/api-docs.md +186 -0
- package/.agent-context/rules/architecture.md +198 -0
- package/.agent-context/rules/database-design.md +202 -0
- package/.agent-context/rules/efficiency-vs-hype.md +143 -0
- package/.agent-context/rules/error-handling.md +234 -0
- package/.agent-context/rules/event-driven.md +226 -0
- package/.agent-context/rules/frontend-architecture.md +66 -0
- package/.agent-context/rules/git-workflow.md +200 -0
- package/.agent-context/rules/microservices.md +174 -0
- package/.agent-context/rules/naming-conv.md +141 -0
- package/.agent-context/rules/performance.md +168 -0
- package/.agent-context/rules/realtime.md +47 -0
- package/.agent-context/rules/security.md +195 -0
- package/.agent-context/rules/testing.md +178 -0
- package/.agent-context/stacks/csharp.md +149 -0
- package/.agent-context/stacks/go.md +181 -0
- package/.agent-context/stacks/java.md +135 -0
- package/.agent-context/stacks/php.md +178 -0
- package/.agent-context/stacks/python.md +153 -0
- package/.agent-context/stacks/ruby.md +80 -0
- package/.agent-context/stacks/rust.md +86 -0
- package/.agent-context/stacks/typescript.md +317 -0
- package/.agent-context/state/architecture-map.md +25 -0
- package/.agent-context/state/dependency-map.md +32 -0
- package/.agent-override.md +36 -0
- package/.agents/workflows/init-project.md +29 -0
- package/.agents/workflows/refactor.md +29 -0
- package/.agents/workflows/review-code.md +29 -0
- package/.cursorrules +140 -0
- package/.gemini/instructions.md +97 -0
- package/.github/ISSUE_TEMPLATE/v1.7-frontend-work-item.yml +54 -0
- package/.github/copilot-instructions.md +104 -0
- package/.github/workflows/benchmark-detection.yml +38 -0
- package/.github/workflows/frontend-usability-gate.yml +36 -0
- package/.github/workflows/release-gate.yml +32 -0
- package/.github/workflows/sbom-compliance.yml +32 -0
- package/.windsurfrules +106 -0
- package/AGENTS.md +131 -0
- package/CONTRIBUTING.md +136 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/bin/agentic-senior-core.js +1147 -0
- package/mcp.json +29 -0
- package/package.json +50 -0
- package/scripts/detection-benchmark.mjs +138 -0
- package/scripts/frontend-usability-audit.mjs +87 -0
- package/scripts/generate-sbom.mjs +61 -0
- package/scripts/init-project.ps1 +105 -0
- package/scripts/init-project.sh +131 -0
- package/scripts/llm-judge.mjs +664 -0
- package/scripts/release-gate.mjs +116 -0
- package/scripts/validate.mjs +554 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Real-time & WebSockets Architecture
|
|
2
|
+
|
|
3
|
+
> WebSockets are stateful tcp connections in a stateless HTTP world. Treat them with extreme caution.
|
|
4
|
+
|
|
5
|
+
## 1. Connection Management & Scaling
|
|
6
|
+
WebSockets hold a persistent connection, consuming file descriptors and memory on the server.
|
|
7
|
+
- **Rule:** NEVER scale WebSockets using a standard load balancer without sticky sessions (unless utilizing an external Pub/Sub service).
|
|
8
|
+
- **Architecture:** Use a dedicated external Pub/Sub layer to broadcast messages across multiple WebSocket server instances.
|
|
9
|
+
- **Standard:** Redis Pub/Sub (Socket.io-redis, any standard Redis adapter).
|
|
10
|
+
- **Enterprise:** NATS, Apache Kafka, or managed services like AWS API Gateway WebSockets / Pusher / Socket.io / Soketi.
|
|
11
|
+
- **Goal:** Any user can connect to Server A, and receive a message published by a background job running on Server B.
|
|
12
|
+
|
|
13
|
+
## 2. Authentication Context
|
|
14
|
+
WebSockets cannot rely on traditional HTTP Authorization headers during the handshake in browser environments (as the WebSocket API does not allow setting custom headers).
|
|
15
|
+
- **Rule:** Authenticate connections explicitly.
|
|
16
|
+
- **Method A (Cookies):** If the application uses HTTPOnly cookies, authentication happens naturally during the initial HTTP upgrade request.
|
|
17
|
+
- **Method B (Tickets/Tokens):** Pass the JWT or an ephemeral ticket in the connection URL query string (`?token=xyz`) or immediately after connecting via a dedicated `auth` JSON payload.
|
|
18
|
+
- **BANNED:** Never trust a client simply because they know the WebSocket URL.
|
|
19
|
+
|
|
20
|
+
## 3. The "No Business Logic in Sockets" Rule
|
|
21
|
+
A WebSocket controller should be treated exactly like an HTTP controller.
|
|
22
|
+
- **Rule:** The WebSocket handler's **only** responsibility is parsing the incoming message, validating the payload schema, and routing it to a Service/Application layer orchestrator.
|
|
23
|
+
- **BANNED:** Directly writing to databases, executing complex business logic, or calling external APIs directly inside the WebSocket event callback.
|
|
24
|
+
|
|
25
|
+
## 4. Heartbeats & Dead Connections
|
|
26
|
+
TCP connections drop silently (e.g., when a user drives through a tunnel).
|
|
27
|
+
- **Rule:** Implement Ping/Pong heartbeats. The server MUST periodically ping the client. If the client fails to respond within the expected window (e.g., 30s), the server MUST forcefully sever the connection to free resources (`ws.terminate()`, not `ws.close()`).
|
|
28
|
+
|
|
29
|
+
## 5. Event Design & Typing
|
|
30
|
+
Treat WebSocket events like a strongly typed REST API.
|
|
31
|
+
- **Rule:** Every WebSocket message MUST have a mandatory `type` or `event` field, and a strongly typed `payload` field defined by a schema (e.g., Zod, JSON Schema).
|
|
32
|
+
- **BANNED:** Emitting arbitrary strings or untyped JSON objects like `{ user_id: 1, message: "hi" }`.
|
|
33
|
+
- **REQUIRED:**
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"event": "message.created",
|
|
37
|
+
"payload": {
|
|
38
|
+
"id": "msg_123",
|
|
39
|
+
"text": "Hello World",
|
|
40
|
+
"timestamp": "2026-03-01T12:00:00Z"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 6. Rate Limiting & Abuse Prevention
|
|
46
|
+
WebSockets are heavily susceptible to DoS attacks because an open connection bypasses traditional WAF rate limiters.
|
|
47
|
+
- **Rule:** You MUST implement message rate limiting at the application logic layer (e.g., maximum 5 messages per second per connection/user). Violators should be instantly disconnected and IP-banned temporarily.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Security — Trust Nothing, Validate Everything
|
|
2
|
+
|
|
3
|
+
> Every user is a potential attacker. Every input is a potential exploit.
|
|
4
|
+
> You are not paranoid — you are professional.
|
|
5
|
+
|
|
6
|
+
## The Zero Trust Input Rule
|
|
7
|
+
|
|
8
|
+
**ALL data crossing a system boundary is untrusted until validated.**
|
|
9
|
+
|
|
10
|
+
System boundaries include:
|
|
11
|
+
- HTTP request bodies, query params, headers, cookies
|
|
12
|
+
- URL path parameters
|
|
13
|
+
- File uploads
|
|
14
|
+
- WebSocket messages
|
|
15
|
+
- Queue/event payloads
|
|
16
|
+
- Environment variables (at startup)
|
|
17
|
+
- Database query results (yes, even these — data could be corrupted)
|
|
18
|
+
- Third-party API responses
|
|
19
|
+
|
|
20
|
+
### Validation Protocol
|
|
21
|
+
```
|
|
22
|
+
External Input → Schema Validation (Zod/Pydantic/Bean Validation) → Typed Internal Object
|
|
23
|
+
|
|
24
|
+
NEVER:
|
|
25
|
+
External Input → Direct use in business logic
|
|
26
|
+
External Input → Direct use in database query
|
|
27
|
+
External Input → Direct interpolation into strings
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Injection Prevention
|
|
33
|
+
|
|
34
|
+
### SQL Injection — Parameterized Queries Only
|
|
35
|
+
```
|
|
36
|
+
❌ DEATH PENALTY:
|
|
37
|
+
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
|
38
|
+
const query = "SELECT * FROM users WHERE name = '" + name + "'";
|
|
39
|
+
|
|
40
|
+
✅ ALWAYS:
|
|
41
|
+
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
42
|
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Rule:** NEVER concatenate or interpolate user input into SQL strings. Use parameterized queries or ORMs. No exceptions. Not even "just for testing."
|
|
46
|
+
|
|
47
|
+
### Command Injection
|
|
48
|
+
```
|
|
49
|
+
❌ DEATH PENALTY:
|
|
50
|
+
exec(`convert ${filename} output.png`);
|
|
51
|
+
|
|
52
|
+
✅ ALWAYS:
|
|
53
|
+
execFile('convert', [filename, 'output.png']);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Rule:** Never pass user input to shell commands via string interpolation. Use argument arrays.
|
|
57
|
+
|
|
58
|
+
### XSS Prevention
|
|
59
|
+
```
|
|
60
|
+
❌ BANNED:
|
|
61
|
+
element.innerHTML = userInput;
|
|
62
|
+
dangerouslySetInnerHTML={{ __html: userInput }};
|
|
63
|
+
|
|
64
|
+
✅ REQUIRED:
|
|
65
|
+
element.textContent = userInput;
|
|
66
|
+
// Or sanitize with DOMPurify if HTML is absolutely needed
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Rule:** Default to text content. Only use HTML injection with explicit sanitization AND a code comment explaining WHY.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Secret Management
|
|
74
|
+
|
|
75
|
+
### Rule: ZERO Secrets in Code
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
❌ INSTANT REJECTION:
|
|
79
|
+
const API_KEY = "sk-abc123def456";
|
|
80
|
+
const DB_PASSWORD = "supersecret";
|
|
81
|
+
const JWT_SECRET = "my-jwt-secret";
|
|
82
|
+
// Even in .env.example with real values
|
|
83
|
+
|
|
84
|
+
✅ REQUIRED:
|
|
85
|
+
const API_KEY = process.env.API_KEY; // Read from environment
|
|
86
|
+
const DB_URL = process.env.DATABASE_URL; // Injected at runtime
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### .env Files
|
|
90
|
+
- `.env` → NEVER committed (must be in `.gitignore`)
|
|
91
|
+
- `.env.example` → Committed with placeholder values ONLY (`API_KEY=your-api-key-here`)
|
|
92
|
+
- `.env.local` → NEVER committed
|
|
93
|
+
- `.env.test` → May be committed with TEST-ONLY non-secret values
|
|
94
|
+
|
|
95
|
+
### Secret Rotation
|
|
96
|
+
If a secret is accidentally committed:
|
|
97
|
+
1. Rotate the secret IMMEDIATELY (not after the PR is merged — NOW)
|
|
98
|
+
2. Remove from git history (`git filter-branch` or BFG)
|
|
99
|
+
3. Add to `.gitignore`
|
|
100
|
+
4. Document the incident
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Authentication & Authorization
|
|
105
|
+
|
|
106
|
+
### Authentication Rules
|
|
107
|
+
1. Never implement custom auth crypto — use established libraries (argon2, bcrypt, Passport, NextAuth)
|
|
108
|
+
2. Hash passwords with **argon2id** (OWASP primary recommendation) — bcrypt only for legacy systems
|
|
109
|
+
- Argon2id: minimum 19 MiB memory, 2 iterations, 1 parallelism
|
|
110
|
+
- bcrypt: minimum cost 12 (legacy systems only — limited to 72 bytes, no memory-hardness)
|
|
111
|
+
- NEVER: MD5, SHA1, plain SHA256, or PBKDF2 without FIPS requirement
|
|
112
|
+
3. Use constant-time comparison for tokens and hashes
|
|
113
|
+
4. Implement rate limiting on auth endpoints (max 5 attempts per minute per IP)
|
|
114
|
+
5. Session tokens must be cryptographically random (≥ 256 bits)
|
|
115
|
+
|
|
116
|
+
### Authorization Rules
|
|
117
|
+
1. **Default deny** — if no rule grants access, deny
|
|
118
|
+
2. **Server-side only** — NEVER trust client-side role checks for security
|
|
119
|
+
3. Check authorization at the service layer, not just the controller
|
|
120
|
+
4. Log all authorization failures with context (userId, resource, action)
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
❌ BANNED: Client-side only authorization
|
|
124
|
+
if (user.role === 'admin') { showDeleteButton(); }
|
|
125
|
+
// Attacker just changes user.role in devtools
|
|
126
|
+
|
|
127
|
+
✅ REQUIRED: Server-side enforcement
|
|
128
|
+
// Controller checks auth, service enforces business rules
|
|
129
|
+
async deleteUser(requesterId: string, targetUserId: string) {
|
|
130
|
+
const requester = await this.userRepo.findById(requesterId);
|
|
131
|
+
if (!requester || requester.role !== Role.ADMIN) {
|
|
132
|
+
throw new ForbiddenError('Insufficient permissions');
|
|
133
|
+
}
|
|
134
|
+
// ... proceed with deletion
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## HTTP Security Headers
|
|
141
|
+
|
|
142
|
+
Every web application MUST include:
|
|
143
|
+
```
|
|
144
|
+
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
|
145
|
+
Content-Security-Policy: default-src 'self'; script-src 'self'
|
|
146
|
+
X-Content-Type-Options: nosniff
|
|
147
|
+
X-Frame-Options: DENY
|
|
148
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
149
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### CORS
|
|
153
|
+
- NEVER use `Access-Control-Allow-Origin: *` in production
|
|
154
|
+
- Whitelist specific origins
|
|
155
|
+
- Be explicit about allowed methods and headers
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## File Upload Security
|
|
160
|
+
|
|
161
|
+
1. Validate MIME type server-side (not just file extension)
|
|
162
|
+
2. Set maximum file size limits
|
|
163
|
+
3. Generate random filenames — never use user-provided filenames
|
|
164
|
+
4. Store uploads outside the web root
|
|
165
|
+
5. Scan for malware if accepting documents
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Supply Chain Security (OWASP 2025 A03)
|
|
170
|
+
|
|
171
|
+
1. **Pin all dependency versions** — use lockfiles, never `*` ranges in production
|
|
172
|
+
2. **Audit dependencies regularly** — `npm audit`, `pip audit`, `composer audit`
|
|
173
|
+
3. **Verify package integrity** — check checksums, use signed packages where available
|
|
174
|
+
4. **Minimize dependency trees** — fewer transitive dependencies = smaller attack surface
|
|
175
|
+
5. **Monitor for CVEs** — automate vulnerability scanning in CI (Dependabot, Snyk, Trivy)
|
|
176
|
+
6. **Review new dependencies** — check maintainer history, download trends, and bus factor before adding
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## The Security Checklist (Quick Reference)
|
|
181
|
+
|
|
182
|
+
Before any code is "done", verify:
|
|
183
|
+
|
|
184
|
+
- [ ] All inputs validated at boundaries with schemas
|
|
185
|
+
- [ ] No string concatenation in queries/commands
|
|
186
|
+
- [ ] No secrets in source code
|
|
187
|
+
- [ ] Authentication uses established libraries
|
|
188
|
+
- [ ] Password hashing uses argon2id (or bcrypt for legacy)
|
|
189
|
+
- [ ] Authorization enforced server-side
|
|
190
|
+
- [ ] Security headers configured
|
|
191
|
+
- [ ] CORS properly restricted
|
|
192
|
+
- [ ] Rate limiting on sensitive endpoints
|
|
193
|
+
- [ ] Error responses don't leak internal details
|
|
194
|
+
- [ ] Logging includes security events (login failures, permission denials)
|
|
195
|
+
- [ ] Dependencies audited for known vulnerabilities
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Testing — Prove It Works, Don't Pray
|
|
2
|
+
|
|
3
|
+
> "It works on my machine" is not a test strategy.
|
|
4
|
+
> Untested code is broken code that hasn't been caught yet.
|
|
5
|
+
|
|
6
|
+
## The Test Pyramid (Enforced)
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
╱ E2E ╲ Few — Slow — Expensive — Fragile
|
|
10
|
+
╱──────────╲ Test critical user journeys only
|
|
11
|
+
╱ Integration ╲ Medium — Test module boundaries
|
|
12
|
+
╱────────────────╲ Database, API contracts, service interactions
|
|
13
|
+
╱ Unit Tests ╲ Many — Fast — Cheap — Stable
|
|
14
|
+
╱──────────────────────╲ Test business logic in isolation
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Ratios
|
|
18
|
+
- **Unit tests:** 70% — fast, isolated, test business rules
|
|
19
|
+
- **Integration tests:** 20% — test boundaries (DB, APIs, modules)
|
|
20
|
+
- **E2E tests:** 10% — test critical user flows only
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What to Test (And What Not To)
|
|
25
|
+
|
|
26
|
+
### ✅ ALWAYS Test
|
|
27
|
+
- Business logic and calculations
|
|
28
|
+
- Input validation rules
|
|
29
|
+
- Edge cases (empty arrays, null values, boundary numbers)
|
|
30
|
+
- Error handling paths (what happens when things fail)
|
|
31
|
+
- State transitions and workflows
|
|
32
|
+
- Authorization rules
|
|
33
|
+
|
|
34
|
+
### ❌ NEVER Test
|
|
35
|
+
- Framework internals (don't test that Express routes work)
|
|
36
|
+
- Simple getters/setters with no logic
|
|
37
|
+
- Third-party library behavior
|
|
38
|
+
- Private implementation details (test behavior, not structure)
|
|
39
|
+
- Database migrations (verify schema, don't test the migration tool)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Test Naming Convention
|
|
44
|
+
|
|
45
|
+
### Pattern: `should [expected behavior] when [condition]`
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
❌ BANNED:
|
|
49
|
+
test('test1')
|
|
50
|
+
test('it works')
|
|
51
|
+
test('calculateDiscount')
|
|
52
|
+
|
|
53
|
+
✅ REQUIRED:
|
|
54
|
+
test('should apply 20% discount when order total exceeds $100')
|
|
55
|
+
test('should throw ValidationError when email format is invalid')
|
|
56
|
+
test('should return empty array when user has no orders')
|
|
57
|
+
test('should deny access when user lacks admin role')
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Rule:** A reader must understand the expected behavior WITHOUT reading the test body.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Test Structure: AAA Pattern
|
|
65
|
+
|
|
66
|
+
Every test follows **Arrange → Act → Assert**:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
test('should calculate shipping as free when order exceeds $50', () => {
|
|
70
|
+
// Arrange — Set up the scenario
|
|
71
|
+
const order = createOrder({ items: [{ price: 60, quantity: 1 }] });
|
|
72
|
+
|
|
73
|
+
// Act — Execute the behavior
|
|
74
|
+
const shippingCost = calculateShipping(order);
|
|
75
|
+
|
|
76
|
+
// Assert — Verify the outcome
|
|
77
|
+
expect(shippingCost).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Rules
|
|
82
|
+
- **ONE assert concept per test** (multiple `expect` calls are fine if they test the same concept)
|
|
83
|
+
- **No logic in tests** (no if/else, no loops, no try/catch)
|
|
84
|
+
- **Tests must be independent** — no shared mutable state, no execution order dependency
|
|
85
|
+
- **Tests must be deterministic** — same input = same result, every time
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Mocking Rules
|
|
90
|
+
|
|
91
|
+
### Mock at Boundaries, Not Everywhere
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
❌ OVER-MOCKING (testing implementation, not behavior):
|
|
95
|
+
test('should call repository.save exactly once', () => {
|
|
96
|
+
await service.createUser(userData);
|
|
97
|
+
expect(repository.save).toHaveBeenCalledTimes(1);
|
|
98
|
+
// If you refactor to call save differently, the test breaks
|
|
99
|
+
// even though the behavior hasn't changed
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
✅ CORRECT (testing behavior):
|
|
103
|
+
test('should persist user and return created user', () => {
|
|
104
|
+
const result = await service.createUser(userData);
|
|
105
|
+
expect(result.id).toBeDefined();
|
|
106
|
+
expect(result.email).toBe(userData.email);
|
|
107
|
+
// You can verify the user exists in the test DB if integration test
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### When to Mock
|
|
112
|
+
- External APIs (payment gateways, email services)
|
|
113
|
+
- Time-dependent operations (use fake timers)
|
|
114
|
+
- Non-deterministic operations (random, UUID)
|
|
115
|
+
|
|
116
|
+
### When NOT to Mock
|
|
117
|
+
- Your own code in the same module ← test the real integration
|
|
118
|
+
- Simple utility functions ← use the real thing
|
|
119
|
+
- Database in integration tests ← use a test database
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Test Data Standards
|
|
124
|
+
|
|
125
|
+
### Use Factories, Not Copy-Pasted Objects
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
❌ BANNED:
|
|
129
|
+
test('should calculate total', () => {
|
|
130
|
+
const order = {
|
|
131
|
+
id: '123',
|
|
132
|
+
userId: '456',
|
|
133
|
+
items: [{ productId: '789', price: 29.99, quantity: 2, name: 'Widget' }],
|
|
134
|
+
status: 'pending',
|
|
135
|
+
createdAt: new Date(),
|
|
136
|
+
updatedAt: new Date(),
|
|
137
|
+
// ... 15 more fields copied from another test
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
✅ REQUIRED:
|
|
142
|
+
test('should calculate total', () => {
|
|
143
|
+
const order = createTestOrder({
|
|
144
|
+
items: [createTestItem({ price: 29.99, quantity: 2 })],
|
|
145
|
+
});
|
|
146
|
+
// Factory fills in all other fields with sensible defaults
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Rules
|
|
151
|
+
- Create factory functions for each domain entity
|
|
152
|
+
- Factories provide sensible defaults — tests override only relevant fields
|
|
153
|
+
- Never use production data in tests
|
|
154
|
+
- Use descriptive, obviously-fake data (email: `test-user@example.com`, not `john@gmail.com`)
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Coverage Expectations
|
|
159
|
+
|
|
160
|
+
| Layer | Min Coverage | What to Focus On |
|
|
161
|
+
|-------|-------------|------------------|
|
|
162
|
+
| Domain / Business Logic | 90%+ | All branching, edge cases, error paths |
|
|
163
|
+
| Application / Service | 80%+ | Orchestration flows, error handling |
|
|
164
|
+
| Transport / Controller | 60%+ | Input validation, error responses |
|
|
165
|
+
| Utilities | 90%+ | All functions and edge cases |
|
|
166
|
+
|
|
167
|
+
**Rule:** Coverage is a floor, not a goal. 100% coverage with bad tests is worse than 80% coverage with good tests. Focus on testing **behavior and edge cases**, not hitting a number.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## When to Skip Tests (Rare)
|
|
172
|
+
|
|
173
|
+
You may skip tests ONLY for:
|
|
174
|
+
- Prototype/spike code (must be labeled `// SPIKE: will be replaced`)
|
|
175
|
+
- Pure UI layout (visual testing is better here — use Storybook/Chromatic)
|
|
176
|
+
- Generated code (test the generator, not the output)
|
|
177
|
+
|
|
178
|
+
Everything else gets tested. No excuses.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# C# / .NET Stack Profile — Modern C#, Minimal Ceremony
|
|
2
|
+
|
|
3
|
+
> C# has evolved massively. Use the modern features.
|
|
4
|
+
> If your code looks like it's from 2015, it's wrong.
|
|
5
|
+
|
|
6
|
+
## Language Version: C# 14+ / .NET 10 LTS
|
|
7
|
+
|
|
8
|
+
.NET 10 is the latest LTS release (November 2025, 3 years support). C# 14 ships with .NET 10. Use modern features: records, primary constructors, extension members, nullable reference types.
|
|
9
|
+
|
|
10
|
+
### Nullable Reference Types (Mandatory)
|
|
11
|
+
```xml
|
|
12
|
+
<!-- In .csproj — ALWAYS enabled -->
|
|
13
|
+
<PropertyGroup>
|
|
14
|
+
<Nullable>enable</Nullable>
|
|
15
|
+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
16
|
+
</PropertyGroup>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Records for DTOs and Value Objects
|
|
20
|
+
```csharp
|
|
21
|
+
// BANNED: Mutable class with manual properties
|
|
22
|
+
public class UserDto {
|
|
23
|
+
public string Name { get; set; }
|
|
24
|
+
public string Email { get; set; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// REQUIRED: Immutable record
|
|
28
|
+
public record CreateUserRequest(string Name, string Email, int Age);
|
|
29
|
+
public record UserResponse(Guid Id, string Name, string Email, DateTime CreatedAt);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Primary Constructors (C# 12+)
|
|
33
|
+
```csharp
|
|
34
|
+
// Clean dependency injection
|
|
35
|
+
public class UserService(IUserRepository userRepository, ILogger<UserService> logger)
|
|
36
|
+
{
|
|
37
|
+
public async Task<UserResponse> CreateAsync(CreateUserRequest request)
|
|
38
|
+
{
|
|
39
|
+
logger.LogInformation("Creating user {Email}", request.Email);
|
|
40
|
+
var user = await userRepository.CreateAsync(request);
|
|
41
|
+
return user.ToResponse();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Validation at Boundaries
|
|
49
|
+
|
|
50
|
+
### Minimal API with FluentValidation
|
|
51
|
+
```csharp
|
|
52
|
+
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
|
|
53
|
+
{
|
|
54
|
+
public CreateUserValidator()
|
|
55
|
+
{
|
|
56
|
+
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
|
|
57
|
+
RuleFor(x => x.Email).NotEmpty().EmailAddress();
|
|
58
|
+
RuleFor(x => x.Age).InclusiveBetween(13, 150);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
app.MapPost("/users", async (CreateUserRequest request, IValidator<CreateUserRequest> validator,
|
|
63
|
+
UserService userService) =>
|
|
64
|
+
{
|
|
65
|
+
var validation = await validator.ValidateAsync(request);
|
|
66
|
+
if (!validation.IsValid)
|
|
67
|
+
return Results.ValidationProblem(validation.ToDictionary());
|
|
68
|
+
|
|
69
|
+
var user = await userService.CreateAsync(request);
|
|
70
|
+
return Results.Created($"/users/{user.Id}", user);
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Project Structure
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
ProjectName/
|
|
80
|
+
├── src/
|
|
81
|
+
│ ├── ProjectName.Api/ # Presentation/Transport layer
|
|
82
|
+
│ │ ├── Program.cs # Entry point + DI setup
|
|
83
|
+
│ │ ├── Endpoints/
|
|
84
|
+
│ │ │ ├── UserEndpoints.cs # Minimal API route definitions
|
|
85
|
+
│ │ │ └── OrderEndpoints.cs
|
|
86
|
+
│ │ ├── Middleware/
|
|
87
|
+
│ │ │ └── ExceptionMiddleware.cs
|
|
88
|
+
│ │ └── appsettings.json
|
|
89
|
+
│ │
|
|
90
|
+
│ ├── ProjectName.Application/ # Business logic layer
|
|
91
|
+
│ │ ├── Users/
|
|
92
|
+
│ │ │ ├── UserService.cs
|
|
93
|
+
│ │ │ ├── CreateUserRequest.cs # DTOs / records
|
|
94
|
+
│ │ │ └── UserResponse.cs
|
|
95
|
+
│ │ └── Common/
|
|
96
|
+
│ │ ├── AppError.cs
|
|
97
|
+
│ │ └── IUnitOfWork.cs
|
|
98
|
+
│ │
|
|
99
|
+
│ ├── ProjectName.Domain/ # Domain entities (no dependencies)
|
|
100
|
+
│ │ ├── Entities/
|
|
101
|
+
│ │ │ └── User.cs
|
|
102
|
+
│ │ └── Interfaces/
|
|
103
|
+
│ │ └── IUserRepository.cs # Repository contracts
|
|
104
|
+
│ │
|
|
105
|
+
│ └── ProjectName.Infrastructure/ # Data access, external services
|
|
106
|
+
│ ├── Persistence/
|
|
107
|
+
│ │ ├── AppDbContext.cs # EF Core DbContext
|
|
108
|
+
│ │ └── UserRepository.cs # Repository implementation
|
|
109
|
+
│ ├── Migrations/
|
|
110
|
+
│ └── DependencyInjection.cs # Extension methods for DI
|
|
111
|
+
│
|
|
112
|
+
└── tests/
|
|
113
|
+
├── ProjectName.UnitTests/
|
|
114
|
+
└── ProjectName.IntegrationTests/
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Preferred Libraries
|
|
120
|
+
|
|
121
|
+
| Need | Library | Why |
|
|
122
|
+
|------|---------|-----|
|
|
123
|
+
| Framework | ASP.NET Core 10 Minimal APIs | LTS, lightweight, OpenAPI 3.1, passkey auth |
|
|
124
|
+
| ORM | EF Core 10 | LINQ, migrations, vector search, JSON types |
|
|
125
|
+
| Validation | FluentValidation | Expressive, testable, separates concerns |
|
|
126
|
+
| Testing | xUnit + NSubstitute + Testcontainers | Industry standard for .NET |
|
|
127
|
+
| Logging | Serilog + structured sinks | Best structured logging for .NET |
|
|
128
|
+
| API docs | Swashbuckle / NSwag | OpenAPI auto-generation |
|
|
129
|
+
| Mapping | Mapster or manual extension methods | Mapster faster than AutoMapper |
|
|
130
|
+
| HTTP client | `IHttpClientFactory` | Pooled, resilient, built-in |
|
|
131
|
+
| Configuration | Options pattern + `IOptions<T>` | Type-safe, validated config |
|
|
132
|
+
| Auth | ASP.NET Core Identity / JWT / Passkeys | Built-in passkey support in .NET 10 |
|
|
133
|
+
| Cloud-native | .NET Aspire | Orchestration, observability, service defaults |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Banned Patterns
|
|
138
|
+
|
|
139
|
+
| Pattern | Why | Alternative |
|
|
140
|
+
|---------|-----|-------------|
|
|
141
|
+
| `Nullable` disabled | NRE everywhere | Always `<Nullable>enable</Nullable>` |
|
|
142
|
+
| `dynamic` type | No compile-time safety | Generics or strong types |
|
|
143
|
+
| Service Locator | Hidden dependencies | Constructor injection |
|
|
144
|
+
| `async void` | Unhandled exceptions crash the app | `async Task` always |
|
|
145
|
+
| `string.Format` for SQL | SQL injection | EF Core LINQ or parameterized |
|
|
146
|
+
| AutoMapper overuse | Magic mapping hides bugs | Manual mapping or Mapster |
|
|
147
|
+
| Throw in constructor | Breaks DI container | Factory methods or validation |
|
|
148
|
+
| `static` classes for services | Untestable, no DI | Interface + DI registration |
|
|
149
|
+
| Controllers with business logic | Layer leak | Thin controllers, services layer |
|