@polymorphism-tech/morph-spec 4.3.0 → 4.3.2
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/CLAUDE.md +155 -0
- package/bin/morph-spec.js +3 -3
- package/bin/task-manager.cjs +102 -14
- package/package.json +2 -1
- package/src/commands/agents/agents-fuse.js +2 -1
- package/src/commands/project/detect-agents.js +31 -1
- package/src/commands/project/detect.js +11 -1
- package/src/commands/project/doctor.js +76 -70
- package/src/commands/project/init.js +9 -2
- package/src/commands/project/update.js +12 -2
- package/src/commands/state/advance-phase.js +19 -4
- package/src/commands/state/state.js +38 -14
- package/src/commands/tasks/task.js +1 -1
- package/src/commands/threads/thread-template.js +1 -1
- package/src/core/state/state-manager.js +19 -15
- package/src/core/templates/template-registry.js +1 -1
- package/src/core/workflows/workflow-detector.js +16 -3
- package/src/lib/checkpoints/checkpoint-hooks.js +8 -3
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/detectors/standards-generator.js +77 -17
- package/src/lib/detectors/structure-detector.js +67 -39
- package/src/lib/generators/recap-generator.js +30 -10
- package/src/lib/validators/validation-runner.js +8 -24
- package/src/utils/hooks-installer.js +69 -0
- package/stacks/blazor-azure/.claude/commands/morph-apply.md +221 -0
- package/stacks/blazor-azure/.claude/commands/morph-archive.md +79 -0
- package/stacks/blazor-azure/.claude/commands/morph-deploy.md +529 -0
- package/stacks/blazor-azure/.claude/commands/morph-infra.md +209 -0
- package/stacks/blazor-azure/.claude/commands/morph-preflight.md +227 -0
- package/stacks/blazor-azure/.claude/commands/morph-proposal.md +122 -0
- package/stacks/blazor-azure/.claude/commands/morph-status.md +86 -0
- package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +122 -0
- package/stacks/blazor-azure/.morph/.morphversion +5 -5
- package/stacks/blazor-azure/.morph/archive/.gitkeep +25 -0
- package/stacks/blazor-azure/.morph/config/config.json +9 -0
- package/stacks/blazor-azure/.morph/features/.gitkeep +25 -0
- package/stacks/blazor-azure/.morph/project/context/README.md +17 -0
- package/stacks/blazor-azure/.morph/schemas/agent.schema.json +296 -0
- package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +220 -0
- package/stacks/blazor-azure/.morph/specs/.gitkeep +20 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/blazor-ui.md +364 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/production.md +415 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/setup.md +418 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/team-orchestration.md +479 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/workflows.md +354 -0
- package/stacks/blazor-azure/.morph/standards/architecture/ddd/aggregates.md +120 -0
- package/stacks/blazor-azure/.morph/standards/architecture/ddd/entities.md +99 -0
- package/stacks/blazor-azure/.morph/standards/architecture/ddd/value-objects.md +124 -0
- package/stacks/blazor-azure/.morph/standards/backend/api/minimal-api.md +494 -0
- package/stacks/blazor-azure/.morph/standards/backend/api/rest.md +492 -0
- package/stacks/blazor-azure/.morph/standards/backend/api/validation.md +88 -0
- package/stacks/blazor-azure/.morph/standards/backend/authentication/passkeys.md +428 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/ef-core.md +199 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/migrations.md +393 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/postgresql/database.md +352 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/repository-patterns.md +528 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/vector-search-rag.md +541 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/async.md +366 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/core.md +117 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/di.md +439 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/resend/resend-email.md +385 -0
- package/stacks/blazor-azure/.morph/standards/context/analytics.md +96 -0
- package/stacks/blazor-azure/.morph/standards/context/bundles.md +110 -0
- package/stacks/blazor-azure/.morph/standards/context/priming.md +78 -0
- package/stacks/blazor-azure/.morph/standards/core/architecture.md +185 -0
- package/stacks/blazor-azure/.morph/standards/core/coding.md +214 -0
- package/stacks/blazor-azure/.morph/standards/core/git-branching-strategy.md +403 -0
- package/stacks/blazor-azure/.morph/standards/core/git.md +185 -0
- package/stacks/blazor-azure/.morph/standards/core/testing.md +295 -0
- package/stacks/blazor-azure/.morph/standards/data/nosql/blob-storage.md +102 -0
- package/stacks/blazor-azure/.morph/standards/data/nosql/cache/redis.md +97 -0
- package/stacks/blazor-azure/.morph/standards/data/nosql/cosmos-db.md +118 -0
- package/stacks/blazor-azure/.morph/standards/data/vector-search/azure-ai-search.md +121 -0
- package/stacks/blazor-azure/.morph/standards/data/vector-search/rag-chunking.md +104 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/design-checklist.md +222 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/fluent-ui.md +137 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/html-conversion.md +184 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/lifecycle.md +195 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/pitfalls.md +198 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/state.md +191 -0
- package/stacks/blazor-azure/.morph/standards/frontend/design-system/animations.md +151 -0
- package/stacks/blazor-azure/.morph/standards/frontend/design-system/naming.md +64 -0
- package/stacks/blazor-azure/.morph/standards/frontend/nextjs/nextjs-patterns.md +198 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/azure.md +624 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/services/functions.md +486 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/services/storage.md +407 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/stacks/blazor-azure/.morph/standards/integration/api/graphql.md +91 -0
- package/stacks/blazor-azure/.morph/standards/integration/api/grpc.md +114 -0
- package/stacks/blazor-azure/.morph/standards/integration/api/rest-design.md +95 -0
- package/stacks/blazor-azure/.morph/standards/integration/event-driven/cqrs.md +101 -0
- package/stacks/blazor-azure/.morph/standards/integration/event-driven/event-sourcing.md +124 -0
- package/stacks/blazor-azure/.morph/standards/integration/event-driven/service-bus.md +95 -0
- package/stacks/blazor-azure/.morph/standards/observability/logging.md +131 -0
- package/stacks/blazor-azure/.morph/standards/observability/metrics.md +121 -0
- package/stacks/blazor-azure/.morph/standards/observability/monitoring.md +114 -0
- package/stacks/blazor-azure/.morph/standards/observability/tracing.md +132 -0
- package/stacks/blazor-azure/.morph/standards/workflows/parallel-execution.md +112 -0
- package/stacks/blazor-azure/.morph/standards/workflows/thread-management.md +113 -0
- package/stacks/blazor-azure/.morph/test-infra/example.bicep +59 -0
- package/stacks/blazor-azure/CLAUDE.md +106 -101
- package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +221 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +79 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +529 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +209 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +227 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +122 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-status.md +86 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +122 -0
- package/stacks/nextjs-supabase/.morph/.morphversion +5 -0
- package/stacks/nextjs-supabase/.morph/config/agents.json +730 -127
- package/stacks/nextjs-supabase/.morph/config/config.json +9 -0
- package/stacks/nextjs-supabase/.morph/project/context/README.md +17 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/blazor-ui.md +364 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/production.md +415 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/setup.md +418 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/team-orchestration.md +479 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/workflows.md +354 -0
- package/stacks/nextjs-supabase/.morph/standards/architecture/ddd/aggregates.md +120 -0
- package/stacks/nextjs-supabase/.morph/standards/architecture/ddd/entities.md +99 -0
- package/stacks/nextjs-supabase/.morph/standards/architecture/ddd/value-objects.md +124 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/api/minimal-api.md +494 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/api/rest.md +492 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/api/validation.md +88 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/authentication/passkeys.md +428 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/ef-core.md +199 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/migrations.md +393 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/postgresql/database.md +352 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/repository-patterns.md +528 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/vector-search-rag.md +541 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/async.md +366 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/core.md +117 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/di.md +439 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/resend/resend-email.md +385 -0
- package/stacks/nextjs-supabase/.morph/standards/context/analytics.md +96 -0
- package/stacks/nextjs-supabase/.morph/standards/context/bundles.md +110 -0
- package/stacks/nextjs-supabase/.morph/standards/context/priming.md +78 -0
- package/stacks/nextjs-supabase/.morph/standards/core/architecture.md +185 -0
- package/stacks/nextjs-supabase/.morph/standards/core/coding.md +214 -0
- package/stacks/nextjs-supabase/.morph/standards/core/git-branching-strategy.md +403 -0
- package/stacks/nextjs-supabase/.morph/standards/core/git.md +185 -0
- package/stacks/nextjs-supabase/.morph/standards/core/testing.md +295 -0
- package/stacks/nextjs-supabase/.morph/standards/data/nosql/blob-storage.md +102 -0
- package/stacks/nextjs-supabase/.morph/standards/data/nosql/cache/redis.md +97 -0
- package/stacks/nextjs-supabase/.morph/standards/data/nosql/cosmos-db.md +118 -0
- package/stacks/nextjs-supabase/.morph/standards/data/vector-search/azure-ai-search.md +121 -0
- package/stacks/nextjs-supabase/.morph/standards/data/vector-search/rag-chunking.md +104 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/design-checklist.md +222 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/fluent-ui.md +137 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/html-conversion.md +184 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/lifecycle.md +195 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/pitfalls.md +198 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/state.md +191 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/design-system/animations.md +151 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/design-system/naming.md +64 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/nextjs/nextjs-patterns.md +198 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/azure.md +624 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/services/functions.md +486 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/services/storage.md +407 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/api/graphql.md +91 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/api/grpc.md +114 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/api/rest-design.md +95 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/event-driven/cqrs.md +101 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/event-driven/event-sourcing.md +124 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/event-driven/service-bus.md +95 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/logging.md +131 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/metrics.md +121 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/monitoring.md +114 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/tracing.md +132 -0
- package/stacks/nextjs-supabase/.morph/standards/workflows/parallel-execution.md +112 -0
- package/stacks/nextjs-supabase/.morph/standards/workflows/thread-management.md +113 -0
- package/stacks/nextjs-supabase/CLAUDE.md +69 -63
- package/stacks/blazor-azure/.morph/templates/.gitkeep +0 -0
- package/stacks/blazor-azure/.morph/templates/infrastructure/github/workflows/cd-prod.yml.hbs +0 -41
- package/stacks/blazor-azure/.morph/templates/infrastructure/github/workflows/cd-staging.yml.hbs +0 -24
- package/stacks/blazor-azure/.morph/templates/infrastructure/github/workflows/ci-build.yml.hbs +0 -23
- package/stacks/nextjs-supabase/.morph/templates/.gitkeep +0 -0
- package/stacks/nextjs-supabase/.morph/templates/infrastructure/github/workflows/cd-prod.yml.hbs +0 -22
- package/stacks/nextjs-supabase/.morph/templates/infrastructure/github/workflows/cd-staging.yml.hbs +0 -22
- package/stacks/nextjs-supabase/.morph/templates/infrastructure/github/workflows/ci-build.yml.hbs +0 -35
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Testing Standards
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 0 (always load)
|
|
5
|
+
> **Keywords:** test, testing, unit, integration, xunit, nunit
|
|
6
|
+
> **Load When:** always
|
|
7
|
+
|
|
8
|
+
Test patterns and best practices for MORPH-SPEC projects
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Test Pyramid
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
/\
|
|
16
|
+
/ \ E2E Tests (10%)
|
|
17
|
+
/____\
|
|
18
|
+
/ \
|
|
19
|
+
/ Integration Tests (30%)
|
|
20
|
+
/________________\
|
|
21
|
+
/ \
|
|
22
|
+
/ Unit Tests (60%) \
|
|
23
|
+
/_______________________\
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Distribution
|
|
27
|
+
|
|
28
|
+
- **Unit Tests (60%)**: Fast, isolated, test single units
|
|
29
|
+
- **Integration Tests (30%)**: Test component interactions
|
|
30
|
+
- **E2E Tests (10%)**: Full user workflows
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Naming Conventions
|
|
35
|
+
|
|
36
|
+
### Format
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
MethodName_Scenario_ExpectedBehavior
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Examples
|
|
43
|
+
|
|
44
|
+
```csharp
|
|
45
|
+
// .NET (xUnit)
|
|
46
|
+
[Fact]
|
|
47
|
+
public void Login_ValidCredentials_ReturnsAuthToken() { }
|
|
48
|
+
|
|
49
|
+
[Fact]
|
|
50
|
+
public void Login_InvalidPassword_ThrowsUnauthorizedException() { }
|
|
51
|
+
|
|
52
|
+
[Theory]
|
|
53
|
+
[InlineData("user@example.com", "password123")]
|
|
54
|
+
public void Register_ValidInput_CreatesUser(string email, string password) { }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// JavaScript (Jest/Vitest)
|
|
59
|
+
describe('UserService', () => {
|
|
60
|
+
it('should return auth token for valid credentials', () => {})
|
|
61
|
+
|
|
62
|
+
it('should throw error for invalid password', () => {})
|
|
63
|
+
|
|
64
|
+
it.each([
|
|
65
|
+
['user@example.com', 'password123'],
|
|
66
|
+
['admin@test.com', 'admin456']
|
|
67
|
+
])('should create user for %s', (email, password) => {})
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Unit Testing Best Practices
|
|
74
|
+
|
|
75
|
+
### Arrange-Act-Assert (AAA)
|
|
76
|
+
|
|
77
|
+
```csharp
|
|
78
|
+
[Fact]
|
|
79
|
+
public void CalculateTotal_WithDiscount_AppliesCorrectPercentage()
|
|
80
|
+
{
|
|
81
|
+
// Arrange
|
|
82
|
+
var cart = new ShoppingCart();
|
|
83
|
+
cart.AddItem(new Item { Price = 100 });
|
|
84
|
+
var discountService = new DiscountService();
|
|
85
|
+
|
|
86
|
+
// Act
|
|
87
|
+
var total = discountService.CalculateTotal(cart, discountPercent: 10);
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
Assert.Equal(90, total);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Mocking Dependencies
|
|
95
|
+
|
|
96
|
+
```csharp
|
|
97
|
+
[Fact]
|
|
98
|
+
public async Task GetUser_ValidId_ReturnsUser()
|
|
99
|
+
{
|
|
100
|
+
// Arrange
|
|
101
|
+
var mockRepo = new Mock<IUserRepository>();
|
|
102
|
+
mockRepo.Setup(r => r.GetByIdAsync(1))
|
|
103
|
+
.ReturnsAsync(new User { Id = 1, Name = "John" });
|
|
104
|
+
|
|
105
|
+
var service = new UserService(mockRepo.Object);
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
var user = await service.GetUserAsync(1);
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
Assert.NotNull(user);
|
|
112
|
+
Assert.Equal("John", user.Name);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Integration Testing
|
|
119
|
+
|
|
120
|
+
### Database Tests
|
|
121
|
+
|
|
122
|
+
```csharp
|
|
123
|
+
public class UserRepositoryTests : IClassFixture<DatabaseFixture>
|
|
124
|
+
{
|
|
125
|
+
private readonly AppDbContext _context;
|
|
126
|
+
|
|
127
|
+
public UserRepositoryTests(DatabaseFixture fixture)
|
|
128
|
+
{
|
|
129
|
+
_context = fixture.CreateContext();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
[Fact]
|
|
133
|
+
public async Task CreateUser_ValidData_SavesToDatabase()
|
|
134
|
+
{
|
|
135
|
+
// Arrange
|
|
136
|
+
var repo = new UserRepository(_context);
|
|
137
|
+
var user = new User { Email = "test@example.com" };
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
await repo.CreateAsync(user);
|
|
141
|
+
await _context.SaveChangesAsync();
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
var saved = await _context.Users.FirstOrDefaultAsync(u => u.Email == "test@example.com");
|
|
145
|
+
Assert.NotNull(saved);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### API Tests
|
|
151
|
+
|
|
152
|
+
```csharp
|
|
153
|
+
public class AuthControllerTests : IClassFixture<WebApplicationFactory<Program>>
|
|
154
|
+
{
|
|
155
|
+
private readonly HttpClient _client;
|
|
156
|
+
|
|
157
|
+
public AuthControllerTests(WebApplicationFactory<Program> factory)
|
|
158
|
+
{
|
|
159
|
+
_client = factory.CreateClient();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[Fact]
|
|
163
|
+
public async Task Login_ValidCredentials_Returns200AndToken()
|
|
164
|
+
{
|
|
165
|
+
// Arrange
|
|
166
|
+
var request = new LoginRequest { Email = "user@test.com", Password = "password" };
|
|
167
|
+
|
|
168
|
+
// Act
|
|
169
|
+
var response = await _client.PostAsJsonAsync("/api/auth/login", request);
|
|
170
|
+
|
|
171
|
+
// Assert
|
|
172
|
+
response.EnsureSuccessStatusCode();
|
|
173
|
+
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
|
|
174
|
+
Assert.NotNull(result?.Token);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## E2E Testing
|
|
182
|
+
|
|
183
|
+
### Playwright Example
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { test, expect } from '@playwright/test';
|
|
187
|
+
|
|
188
|
+
test('user can login and access dashboard', async ({ page }) => {
|
|
189
|
+
// Navigate to login
|
|
190
|
+
await page.goto('/login');
|
|
191
|
+
|
|
192
|
+
// Fill credentials
|
|
193
|
+
await page.fill('input[name="email"]', 'user@example.com');
|
|
194
|
+
await page.fill('input[name="password"]', 'password123');
|
|
195
|
+
|
|
196
|
+
// Submit
|
|
197
|
+
await page.click('button[type="submit"]');
|
|
198
|
+
|
|
199
|
+
// Verify redirect to dashboard
|
|
200
|
+
await expect(page).toHaveURL('/dashboard');
|
|
201
|
+
await expect(page.locator('h1')).toContainText('Dashboard');
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Test Coverage
|
|
208
|
+
|
|
209
|
+
### Targets
|
|
210
|
+
|
|
211
|
+
- **Statements**: ≥ 80%
|
|
212
|
+
- **Branches**: ≥ 70%
|
|
213
|
+
- **Functions**: ≥ 80%
|
|
214
|
+
- **Lines**: ≥ 80%
|
|
215
|
+
|
|
216
|
+
### Running Coverage
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# .NET
|
|
220
|
+
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
|
221
|
+
|
|
222
|
+
# JavaScript
|
|
223
|
+
npm run test:coverage
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Best Practices
|
|
229
|
+
|
|
230
|
+
### DO
|
|
231
|
+
|
|
232
|
+
✅ Write tests before or with code (TDD)
|
|
233
|
+
✅ Test edge cases (null, empty, invalid input)
|
|
234
|
+
✅ Use descriptive test names
|
|
235
|
+
✅ Keep tests fast (< 1s per unit test)
|
|
236
|
+
✅ Mock external dependencies
|
|
237
|
+
✅ Test one thing per test
|
|
238
|
+
|
|
239
|
+
### DON'T
|
|
240
|
+
|
|
241
|
+
❌ Test implementation details (test behavior)
|
|
242
|
+
❌ Rely on test execution order
|
|
243
|
+
❌ Use real databases in unit tests
|
|
244
|
+
❌ Hardcode magic values (use constants)
|
|
245
|
+
❌ Skip tests (fix or remove)
|
|
246
|
+
❌ Write tests that depend on external services
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Test Fixtures and Cleanup
|
|
251
|
+
|
|
252
|
+
### xUnit
|
|
253
|
+
|
|
254
|
+
```csharp
|
|
255
|
+
public class DatabaseFixture : IDisposable
|
|
256
|
+
{
|
|
257
|
+
public AppDbContext CreateContext()
|
|
258
|
+
{
|
|
259
|
+
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
260
|
+
.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")
|
|
261
|
+
.Options;
|
|
262
|
+
|
|
263
|
+
return new AppDbContext(options);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public void Dispose()
|
|
267
|
+
{
|
|
268
|
+
// Cleanup
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Jest
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
// Setup before each test
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
afterEach(() => {
|
|
281
|
+
// Cleanup after each test
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
beforeAll(() => {
|
|
285
|
+
// Setup once before all tests
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
afterAll(() => {
|
|
289
|
+
// Cleanup once after all tests
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Data Standard: Azure Blob Storage
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Object storage for files, images, documents, and large binary data.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
```xml
|
|
8
|
+
<PackageReference Include="Azure.Storage.Blobs" Version="12.*" />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```csharp
|
|
12
|
+
// Program.cs
|
|
13
|
+
builder.Services.AddSingleton(new BlobServiceClient(
|
|
14
|
+
new Uri(config["Storage:AccountUrl"]),
|
|
15
|
+
new DefaultAzureCredential())); // Managed Identity — no connection string
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Service Pattern
|
|
19
|
+
```csharp
|
|
20
|
+
public class BlobStorageService : IFileStorageService
|
|
21
|
+
{
|
|
22
|
+
private readonly BlobContainerClient _container;
|
|
23
|
+
|
|
24
|
+
public BlobStorageService(BlobServiceClient client, IConfiguration config)
|
|
25
|
+
{
|
|
26
|
+
_container = client.GetBlobContainerClient(config["Storage:ContainerName"]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async Task<string> UploadAsync(Stream content, string fileName, string contentType)
|
|
30
|
+
{
|
|
31
|
+
var blobName = $"{DateTime.UtcNow:yyyy/MM/dd}/{Guid.NewGuid()}/{fileName}";
|
|
32
|
+
var blob = _container.GetBlobClient(blobName);
|
|
33
|
+
|
|
34
|
+
await blob.UploadAsync(content, new BlobHttpHeaders { ContentType = contentType });
|
|
35
|
+
return blobName; // Return blob path, not URL
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async Task<Stream> DownloadAsync(string blobName)
|
|
39
|
+
{
|
|
40
|
+
var blob = _container.GetBlobClient(blobName);
|
|
41
|
+
var response = await blob.DownloadAsync();
|
|
42
|
+
return response.Value.Content;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async Task DeleteAsync(string blobName)
|
|
46
|
+
{
|
|
47
|
+
var blob = _container.GetBlobClient(blobName);
|
|
48
|
+
await blob.DeleteIfExistsAsync();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public Uri GenerateSasUrl(string blobName, TimeSpan expiry)
|
|
52
|
+
{
|
|
53
|
+
var blob = _container.GetBlobClient(blobName);
|
|
54
|
+
return blob.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.UtcNow.Add(expiry));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Security Requirements
|
|
60
|
+
|
|
61
|
+
### Always Use Managed Identity
|
|
62
|
+
```csharp
|
|
63
|
+
// ✅ Managed Identity — no secrets
|
|
64
|
+
new BlobServiceClient(storageUri, new DefaultAzureCredential())
|
|
65
|
+
|
|
66
|
+
// ❌ Connection string — avoid
|
|
67
|
+
new BlobServiceClient(connectionString)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### SAS Tokens for Client Access
|
|
71
|
+
```csharp
|
|
72
|
+
// Generate short-lived SAS for client-side uploads
|
|
73
|
+
var sasUri = blob.GenerateSasUri(BlobSasPermissions.Write, DateTimeOffset.UtcNow.AddMinutes(15));
|
|
74
|
+
// Send SAS URL to client — they upload directly
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Container Access Levels
|
|
78
|
+
- `Private`: No public access (default for user documents)
|
|
79
|
+
- `Blob`: Public read for specific blobs (CDN-served images)
|
|
80
|
+
- `Container`: Public read for all blobs (avoid in most cases)
|
|
81
|
+
|
|
82
|
+
## Content Types + Validation
|
|
83
|
+
```csharp
|
|
84
|
+
private static readonly HashSet<string> AllowedTypes = ["image/jpeg", "image/png", "application/pdf"];
|
|
85
|
+
private const long MaxFileSizeBytes = 10 * 1024 * 1024; // 10 MB
|
|
86
|
+
|
|
87
|
+
public async Task<string> UploadValidatedAsync(IFormFile file)
|
|
88
|
+
{
|
|
89
|
+
if (!AllowedTypes.Contains(file.ContentType))
|
|
90
|
+
throw new ValidationException($"File type {file.ContentType} not allowed");
|
|
91
|
+
if (file.Length > MaxFileSizeBytes)
|
|
92
|
+
throw new ValidationException("File exceeds 10 MB limit");
|
|
93
|
+
|
|
94
|
+
return await UploadAsync(file.OpenReadStream(), file.FileName, file.ContentType);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Blob Lifecycle (Cost Management)
|
|
99
|
+
- Move to Cool tier after 30 days
|
|
100
|
+
- Move to Archive tier after 90 days
|
|
101
|
+
- Delete after 365 days (unless legal hold)
|
|
102
|
+
- Configure via Storage Account Lifecycle Management rules (Bicep)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Data Standard: Redis Cache (Azure Cache for Redis)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Distributed cache for session data, rate limiting, and frequently-read data.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
```xml
|
|
8
|
+
<PackageReference Include="StackExchange.Redis" Version="2.*" />
|
|
9
|
+
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.*" />
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```csharp
|
|
13
|
+
// Program.cs
|
|
14
|
+
builder.Services.AddStackExchangeRedisCache(opts =>
|
|
15
|
+
{
|
|
16
|
+
opts.Configuration = config["Redis:ConnectionString"]; // from Key Vault
|
|
17
|
+
opts.InstanceName = "myapp:"; // Prefix to avoid key collisions
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Cache-Aside Pattern
|
|
22
|
+
```csharp
|
|
23
|
+
public class CachedProductService : IProductService
|
|
24
|
+
{
|
|
25
|
+
private readonly IDistributedCache _cache;
|
|
26
|
+
private readonly IProductRepository _repository;
|
|
27
|
+
|
|
28
|
+
public async Task<Product?> GetAsync(Guid id, CancellationToken ct = default)
|
|
29
|
+
{
|
|
30
|
+
var cacheKey = $"product:{id}";
|
|
31
|
+
|
|
32
|
+
// Try cache first
|
|
33
|
+
var cached = await _cache.GetStringAsync(cacheKey, ct);
|
|
34
|
+
if (cached != null)
|
|
35
|
+
return JsonSerializer.Deserialize<Product>(cached);
|
|
36
|
+
|
|
37
|
+
// Cache miss → read from DB
|
|
38
|
+
var product = await _repository.GetAsync(id, ct);
|
|
39
|
+
if (product == null) return null;
|
|
40
|
+
|
|
41
|
+
// Cache with TTL
|
|
42
|
+
await _cache.SetStringAsync(cacheKey,
|
|
43
|
+
JsonSerializer.Serialize(product),
|
|
44
|
+
new DistributedCacheEntryOptions
|
|
45
|
+
{
|
|
46
|
+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
|
|
47
|
+
SlidingExpiration = TimeSpan.FromMinutes(5)
|
|
48
|
+
}, ct);
|
|
49
|
+
|
|
50
|
+
return product;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async Task InvalidateAsync(Guid id, CancellationToken ct = default)
|
|
54
|
+
=> await _cache.RemoveAsync($"product:{id}", ct);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## TTL Strategy
|
|
59
|
+
| Data Type | TTL | Rationale |
|
|
60
|
+
|-----------|-----|-----------|
|
|
61
|
+
| User session | 30 min sliding | Active session window |
|
|
62
|
+
| Product catalog | 15 min absolute | Inventory changes |
|
|
63
|
+
| Rate limit counters | 1 min | Per-minute limiting |
|
|
64
|
+
| Computed aggregates | 1 hour | Expensive query results |
|
|
65
|
+
| Reference data | 24 hours | Config rarely changes |
|
|
66
|
+
|
|
67
|
+
## Key Naming Convention
|
|
68
|
+
```
|
|
69
|
+
{service}:{entity}:{id}
|
|
70
|
+
{service}:{entity}:list:{filter}
|
|
71
|
+
{service}:ratelimit:{userId}:{endpoint}
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
product:detail:550e8400-e29b-41d4-a716
|
|
75
|
+
product:list:category:electronics
|
|
76
|
+
auth:ratelimit:user123:login
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Rate Limiting Pattern
|
|
80
|
+
```csharp
|
|
81
|
+
public async Task<bool> IsRateLimitedAsync(string userId, string action, int maxPerMinute)
|
|
82
|
+
{
|
|
83
|
+
var key = $"ratelimit:{action}:{userId}:{DateTime.UtcNow:yyyyMMddHHmm}";
|
|
84
|
+
var db = _redis.GetDatabase();
|
|
85
|
+
|
|
86
|
+
var count = await db.StringIncrementAsync(key);
|
|
87
|
+
if (count == 1) await db.KeyExpireAsync(key, TimeSpan.FromMinutes(1));
|
|
88
|
+
|
|
89
|
+
return count > maxPerMinute;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Anti-Patterns
|
|
94
|
+
- Never cache sensitive data (passwords, tokens, PII) without encryption
|
|
95
|
+
- Never use Redis as primary store — it's a cache, not a database
|
|
96
|
+
- Never omit TTL — unbounded keys fill memory
|
|
97
|
+
- Never catch RedisException silently — degrade gracefully to DB
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Data Standard: Azure Cosmos DB
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
NoSQL document database — use for high-throughput, globally distributed, or schema-flexible data.
|
|
5
|
+
|
|
6
|
+
## When to Use Cosmos DB
|
|
7
|
+
✅ Event store (append-only, high write throughput)
|
|
8
|
+
✅ User activity/session data (variable schema)
|
|
9
|
+
✅ Multi-region writes required
|
|
10
|
+
✅ JSON documents with flexible schema
|
|
11
|
+
❌ Relational data with complex joins (use Azure SQL)
|
|
12
|
+
❌ Small datasets (cost inefficient)
|
|
13
|
+
|
|
14
|
+
## Container Design
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"partitionKey": "/tenantId",
|
|
18
|
+
"indexingPolicy": {
|
|
19
|
+
"includedPaths": [{ "path": "/*" }],
|
|
20
|
+
"excludedPaths": [
|
|
21
|
+
{ "path": "/largePayload/*" },
|
|
22
|
+
{ "path": "/_etag/?" }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Partition Key Strategy
|
|
29
|
+
- High cardinality: userId, tenantId, orderId
|
|
30
|
+
- Even distribution: avoid hot partitions (e.g., /status = "active" for 90% of docs)
|
|
31
|
+
- Query alignment: partition key should appear in most queries
|
|
32
|
+
|
|
33
|
+
## SDK Setup
|
|
34
|
+
```csharp
|
|
35
|
+
// Program.cs
|
|
36
|
+
builder.Services.AddSingleton(sp =>
|
|
37
|
+
new CosmosClient(
|
|
38
|
+
config["CosmosDb:ConnectionString"],
|
|
39
|
+
new CosmosClientOptions
|
|
40
|
+
{
|
|
41
|
+
SerializerOptions = new CosmosSerializationOptions
|
|
42
|
+
{
|
|
43
|
+
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
|
|
44
|
+
},
|
|
45
|
+
ApplicationName = "MyApp"
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
builder.Services.AddSingleton<ICosmosRepository<Order>>(sp =>
|
|
49
|
+
new CosmosRepository<Order>(
|
|
50
|
+
sp.GetRequiredService<CosmosClient>(),
|
|
51
|
+
config["CosmosDb:DatabaseId"],
|
|
52
|
+
"orders"));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Repository Pattern
|
|
56
|
+
```csharp
|
|
57
|
+
public class CosmosRepository<T> where T : CosmosDocument
|
|
58
|
+
{
|
|
59
|
+
private readonly Container _container;
|
|
60
|
+
|
|
61
|
+
public async Task<T?> GetAsync(string id, string partitionKey)
|
|
62
|
+
{
|
|
63
|
+
try
|
|
64
|
+
{
|
|
65
|
+
var response = await _container.ReadItemAsync<T>(id, new PartitionKey(partitionKey));
|
|
66
|
+
return response.Resource;
|
|
67
|
+
}
|
|
68
|
+
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
|
69
|
+
{
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async Task<IReadOnlyList<T>> QueryAsync(string sql, Dictionary<string, object>? parameters = null)
|
|
75
|
+
{
|
|
76
|
+
var query = new QueryDefinition(sql);
|
|
77
|
+
if (parameters != null)
|
|
78
|
+
foreach (var (k, v) in parameters) query = query.WithParameter(k, v);
|
|
79
|
+
|
|
80
|
+
var results = new List<T>();
|
|
81
|
+
using var feed = _container.GetItemQueryIterator<T>(query);
|
|
82
|
+
while (feed.HasMoreResults)
|
|
83
|
+
{
|
|
84
|
+
var page = await feed.ReadNextAsync();
|
|
85
|
+
results.AddRange(page);
|
|
86
|
+
}
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async Task UpsertAsync(T document)
|
|
91
|
+
=> await _container.UpsertItemAsync(document, new PartitionKey(document.PartitionKey));
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Document Base Class
|
|
96
|
+
```csharp
|
|
97
|
+
public abstract class CosmosDocument
|
|
98
|
+
{
|
|
99
|
+
[JsonPropertyName("id")]
|
|
100
|
+
public string Id { get; set; } = Guid.NewGuid().ToString();
|
|
101
|
+
|
|
102
|
+
[JsonPropertyName("_partitionKey")]
|
|
103
|
+
public abstract string PartitionKey { get; }
|
|
104
|
+
|
|
105
|
+
[JsonPropertyName("type")]
|
|
106
|
+
public string Type => GetType().Name;
|
|
107
|
+
|
|
108
|
+
[JsonPropertyName("createdAt")]
|
|
109
|
+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Cost Optimization
|
|
114
|
+
- Enable TTL on transient data (session, temp tokens)
|
|
115
|
+
- Use serverless mode for dev/test environments
|
|
116
|
+
- Index only queried paths (exclude large blobs)
|
|
117
|
+
- Batch operations with TransactionalBatch for same partition
|
|
118
|
+
- Use bulk execution for large imports
|