624 lines
27 KiB
Markdown
624 lines
27 KiB
Markdown
|
|
# dd0c/portal — Test Architecture & TDD Strategy
|
||
|
|
**Product:** Lightweight Internal Developer Portal
|
||
|
|
**Phase:** 6 — Architecture Design
|
||
|
|
**Date:** 2026-02-28
|
||
|
|
**Status:** Draft
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. Testing Philosophy & TDD Workflow
|
||
|
|
|
||
|
|
### Core Principle
|
||
|
|
|
||
|
|
dd0c/portal's most critical logic — ownership inference, discovery reconciliation, and confidence scoring — is pure algorithmic code with well-defined inputs and outputs. This is ideal TDD territory. The test suite is the specification.
|
||
|
|
|
||
|
|
The product's >80% discovery accuracy target is not a QA metric — it's a product promise. Tests enforce it continuously.
|
||
|
|
|
||
|
|
### Red-Green-Refactor Adapted to This Product
|
||
|
|
|
||
|
|
```
|
||
|
|
RED → Write a failing test that encodes a discovery heuristic or ownership rule
|
||
|
|
GREEN → Write the minimum code to pass it (no clever abstractions yet)
|
||
|
|
REFACTOR → Clean up once the rule is proven correct against real-world fixtures
|
||
|
|
```
|
||
|
|
|
||
|
|
**Adapted cycle for discovery heuristics:**
|
||
|
|
|
||
|
|
1. Capture a real-world failure case (e.g., "Lambda functions named `payment-*` were not grouped into a service")
|
||
|
|
2. Write a unit test encoding the expected grouping behavior using a fixture of that Lambda response
|
||
|
|
3. Fix the heuristic
|
||
|
|
4. Add the fixture to the regression suite permanently
|
||
|
|
|
||
|
|
This means every production accuracy bug becomes a permanent test. The test suite grows as a living record of every edge case the discovery engine has encountered.
|
||
|
|
|
||
|
|
### When to Write Tests First vs. Integration Tests Lead
|
||
|
|
|
||
|
|
| Scenario | Approach | Rationale |
|
||
|
|
|----------|----------|-----------|
|
||
|
|
| Ownership scoring algorithm | Unit-first TDD | Pure function, deterministic, no I/O |
|
||
|
|
| Discovery heuristics (CFN → service mapping) | Unit-first TDD | Deterministic logic over fixture data |
|
||
|
|
| GitHub GraphQL query construction | Unit-first TDD | Query builder logic is pure |
|
||
|
|
| AWS API pagination handling | Integration-first | Behavior depends on real API shape |
|
||
|
|
| Meilisearch index sync | Integration-first | Depends on Meilisearch document model |
|
||
|
|
| DynamoDB schema migrations | Integration-first | Requires real DynamoDB Local behavior |
|
||
|
|
| WebSocket progress events | E2E-first | Requires full pipeline to be meaningful |
|
||
|
|
| Stripe webhook handling | Integration-first | Depends on Stripe event payload shape |
|
||
|
|
|
||
|
|
### Test Naming Conventions
|
||
|
|
|
||
|
|
All tests follow the pattern: `[unit under test]_[scenario]_[expected outcome]`
|
||
|
|
|
||
|
|
**TypeScript/Node.js (Jest):**
|
||
|
|
```typescript
|
||
|
|
describe('OwnershipInferenceEngine', () => {
|
||
|
|
describe('scoreOwnership', () => {
|
||
|
|
it('returns_primary_owner_when_codeowners_present_with_high_confidence', () => {})
|
||
|
|
it('marks_service_unowned_when_top_score_below_threshold', () => {})
|
||
|
|
it('marks_service_ambiguous_when_top_two_scores_within_tolerance', () => {})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
**Python (pytest):**
|
||
|
|
```python
|
||
|
|
class TestOwnershipScorer:
|
||
|
|
def test_codeowners_signal_weighted_highest_among_all_signals(self): ...
|
||
|
|
def test_git_blame_frequency_used_when_codeowners_absent(self): ...
|
||
|
|
def test_confidence_below_threshold_flags_service_as_unowned(self): ...
|
||
|
|
```
|
||
|
|
|
||
|
|
**File naming:**
|
||
|
|
- Unit tests: `*.test.ts` / `test_*.py` co-located with source
|
||
|
|
- Integration tests: `*.integration.test.ts` / `test_*_integration.py` in `tests/integration/`
|
||
|
|
- E2E tests: `tests/e2e/*.spec.ts` (Playwright)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Test Pyramid
|
||
|
|
|
||
|
|
### Recommended Ratio: 70 / 20 / 10
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────┐
|
||
|
|
│ E2E / Smoke│ 10% (~30 tests)
|
||
|
|
│ (Playwright)│ Critical user journeys only
|
||
|
|
├─────────────┤
|
||
|
|
│ Integration │ 20% (~80 tests)
|
||
|
|
│ (real deps) │ Service boundaries, API contracts
|
||
|
|
├─────────────┤
|
||
|
|
│ Unit │ 70% (~280 tests)
|
||
|
|
│ (pure logic)│ All heuristics, scoring, parsing
|
||
|
|
└─────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### Unit Test Targets (per component)
|
||
|
|
|
||
|
|
| Component | Language | Test Framework | Target Coverage |
|
||
|
|
|-----------|----------|---------------|----------------|
|
||
|
|
| AWS Scanner (heuristics) | Python | pytest | 90% |
|
||
|
|
| GitHub Scanner (parsers) | Node.js | Jest | 90% |
|
||
|
|
| Reconciliation Engine | Node.js | Jest | 85% |
|
||
|
|
| Ownership Inference | Python | pytest | 95% |
|
||
|
|
| Portal API (route handlers) | Node.js | Jest + Supertest | 80% |
|
||
|
|
| Search proxy + cache logic | Node.js | Jest | 85% |
|
||
|
|
| Slack Bot command handlers | Node.js | Jest | 80% |
|
||
|
|
| Feature flag evaluation | Node.js/Python | Jest/pytest | 95% |
|
||
|
|
| Governance policy engine | Node.js | Jest | 95% |
|
||
|
|
| Schema migration validators | Node.js | Jest | 100% |
|
||
|
|
|
||
|
|
### Integration Test Boundaries
|
||
|
|
|
||
|
|
| Boundary | What to Test | Tool |
|
||
|
|
|----------|-------------|------|
|
||
|
|
| Discovery → GitHub API | GraphQL query shape, pagination, rate limit handling | MSW (mock service worker) or nock |
|
||
|
|
| Discovery → AWS APIs | boto3 call sequences, pagination, error handling | moto (AWS mock library) |
|
||
|
|
| Reconciler → PostgreSQL | Upsert logic, conflict resolution, RLS enforcement | Testcontainers (PostgreSQL) |
|
||
|
|
| Inference → PostgreSQL | Ownership write, confidence update, correction propagation | Testcontainers (PostgreSQL) |
|
||
|
|
| API → Meilisearch | Index sync, search query construction, tenant filter injection | Meilisearch test instance (Docker) |
|
||
|
|
| API → Redis | Cache set/get/invalidation, TTL behavior | ioredis-mock or Testcontainers (Redis) |
|
||
|
|
| Slack Bot → Portal API | Command → search → format response | Supertest against local API |
|
||
|
|
| Stripe webhook → API | Subscription activation, plan change, cancellation | Stripe CLI webhook forwarding |
|
||
|
|
|
||
|
|
### E2E / Smoke Test Scenarios
|
||
|
|
|
||
|
|
1. Full onboarding: GitHub OAuth → AWS connection → discovery trigger → catalog populated
|
||
|
|
2. Cmd+K search returns results in <200ms after discovery
|
||
|
|
3. Ownership correction propagates to similar services
|
||
|
|
4. Slack `/dd0c who owns` returns correct owner
|
||
|
|
5. Discovery accuracy: synthetic org with known ground truth scores >80%
|
||
|
|
6. Governance strict mode: discovery populates pending queue, not catalog directly
|
||
|
|
7. Panic mode: all catalog writes return 503
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Unit Test Strategy (Per Component)
|
||
|
|
|
||
|
|
### 3.1 AWS Scanner (Python / pytest)
|
||
|
|
|
||
|
|
**What to test:**
|
||
|
|
- Resource-to-service grouping heuristics (the core logic)
|
||
|
|
- Confidence score assignment per signal type
|
||
|
|
- Pagination handling for each AWS API
|
||
|
|
- Cross-region scan aggregation
|
||
|
|
- Error handling for throttling, missing permissions, empty accounts
|
||
|
|
|
||
|
|
**Key test cases:**
|
||
|
|
|
||
|
|
```python
|
||
|
|
# tests/unit/test_cfn_scanner.py
|
||
|
|
|
||
|
|
class TestCloudFormationScanner:
|
||
|
|
def test_stack_name_becomes_service_name_with_high_confidence(self):
|
||
|
|
# Given a CFN stack named "payment-api"
|
||
|
|
# Expect service entity with name="payment-api", confidence=0.95
|
||
|
|
|
||
|
|
def test_stack_tags_extracted_as_service_metadata(self):
|
||
|
|
# Given stack with tags {"service": "payment", "team": "payments"}
|
||
|
|
# Expect service.metadata includes both tags
|
||
|
|
|
||
|
|
def test_stacks_in_multiple_regions_deduplicated_by_name(self):
|
||
|
|
# Given same stack name in us-east-1 and us-west-2
|
||
|
|
# Expect single service entity with both regions in infrastructure
|
||
|
|
|
||
|
|
def test_deleted_stacks_excluded_from_results(self):
|
||
|
|
# Given stack with status DELETE_COMPLETE
|
||
|
|
# Expect it is not included in discovered services
|
||
|
|
|
||
|
|
def test_pagination_fetches_all_stacks_beyond_first_page(self):
|
||
|
|
# Given mock returning 2 pages of stacks
|
||
|
|
# Expect all stacks from both pages are processed
|
||
|
|
|
||
|
|
class TestLambdaScanner:
|
||
|
|
def test_lambdas_with_shared_prefix_grouped_into_single_service(self):
|
||
|
|
# Given ["payment-webhook", "payment-processor", "payment-refund"]
|
||
|
|
# Expect single service "payment" with confidence=0.60
|
||
|
|
|
||
|
|
def test_lambda_with_apigw_trigger_gets_higher_confidence(self):
|
||
|
|
# Given Lambda with API Gateway event source mapping
|
||
|
|
# Expect confidence=0.85 (not 0.60)
|
||
|
|
|
||
|
|
def test_standalone_lambda_without_prefix_pattern_kept_as_individual(self):
|
||
|
|
# Given Lambda named "data-export-job" with no siblings
|
||
|
|
# Expect individual service entity, not grouped
|
||
|
|
|
||
|
|
class TestServiceGroupingHeuristics:
|
||
|
|
def test_cfn_stack_takes_priority_over_ecs_service_for_same_name(self):
|
||
|
|
# Given CFN stack "payment-api" AND ECS service "payment-api"
|
||
|
|
# Expect single service entity (not duplicate), source=cloudformation
|
||
|
|
|
||
|
|
def test_explicit_github_repo_tag_overrides_name_matching(self):
|
||
|
|
# Given AWS resource with tag github_repo="acme/payments-v2"
|
||
|
|
# Expect repo_link="acme/payments-v2" with confidence=0.95
|
||
|
|
# (not fuzzy name match result)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Mocking strategy:**
|
||
|
|
- Use `moto` to mock all boto3 calls — no real AWS calls in unit tests
|
||
|
|
- Fixture files in `tests/fixtures/aws/` contain realistic API response payloads
|
||
|
|
- Each fixture named after the scenario: `cfn_stacks_multi_region.json`, `lambda_functions_with_apigw.json`
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_aws(aws_credentials):
|
||
|
|
with mock_cloudformation(), mock_ecs(), mock_lambda_():
|
||
|
|
yield
|
||
|
|
|
||
|
|
def test_full_scan_produces_expected_service_count(mock_aws, cfn_fixture):
|
||
|
|
setup_mock_cfn_stacks(cfn_fixture)
|
||
|
|
result = AWSScanner(tenant_id="test", role_arn="arn:aws:iam::123:role/test").scan()
|
||
|
|
assert len(result.services) == cfn_fixture["expected_service_count"]
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.2 GitHub Scanner (Node.js / Jest)
|
||
|
|
|
||
|
|
**What to test:**
|
||
|
|
- GraphQL query construction and batching
|
||
|
|
- CODEOWNERS file parsing (all valid formats)
|
||
|
|
- README first-paragraph extraction
|
||
|
|
- Deploy workflow target extraction
|
||
|
|
- Rate limit detection and backoff
|
||
|
|
|
||
|
|
**Key test cases:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tests/unit/github-scanner/codeowners-parser.test.ts
|
||
|
|
|
||
|
|
describe('CODEOWNERSParser', () => {
|
||
|
|
it('parses_simple_wildcard_ownership_to_team', () => {
|
||
|
|
const input = '* @acme/platform-team'
|
||
|
|
expect(parse(input)).toEqual([{ pattern: '*', owners: ['@acme/platform-team'] }])
|
||
|
|
})
|
||
|
|
|
||
|
|
it('parses_path_specific_ownership', () => {
|
||
|
|
const input = '/src/payments/ @acme/payments-team'
|
||
|
|
expect(parse(input)).toEqual([{ pattern: '/src/payments/', owners: ['@acme/payments-team'] }])
|
||
|
|
})
|
||
|
|
|
||
|
|
it('handles_multiple_owners_per_pattern', () => {
|
||
|
|
const input = '*.ts @acme/frontend @acme/platform'
|
||
|
|
expect(parse(input).owners).toHaveLength(2)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('ignores_comment_lines', () => {
|
||
|
|
const input = '# This is a comment\n* @acme/team'
|
||
|
|
expect(parse(input)).toHaveLength(1)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('returns_empty_array_for_missing_codeowners_file', () => {
|
||
|
|
expect(parse(null)).toEqual([])
|
||
|
|
})
|
||
|
|
|
||
|
|
it('handles_individual_user_ownership_not_just_teams', () => {
|
||
|
|
const input = '* @sarah-chen'
|
||
|
|
expect(parse(input)[0].owners[0]).toBe('@sarah-chen')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('READMEExtractor', () => {
|
||
|
|
it('extracts_first_non_heading_non_badge_paragraph', () => {
|
||
|
|
const readme = `# Payment Gateway\n\n\n\nHandles Stripe checkout flows.`
|
||
|
|
expect(extractDescription(readme)).toBe('Handles Stripe checkout flows.')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('returns_null_when_readme_has_only_headings_and_badges', () => {
|
||
|
|
const readme = `# Title\n\n`
|
||
|
|
expect(extractDescription(readme)).toBeNull()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('WorkflowTargetExtractor', () => {
|
||
|
|
it('extracts_ecs_service_name_from_deploy_workflow', () => {
|
||
|
|
const yaml = loadFixture('deploy-workflow-ecs.yml')
|
||
|
|
expect(extractDeployTarget(yaml)).toEqual({
|
||
|
|
type: 'ecs_service',
|
||
|
|
name: 'payment-api',
|
||
|
|
cluster: 'production'
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
it('extracts_lambda_function_name_from_serverless_deploy', () => {
|
||
|
|
const yaml = loadFixture('deploy-workflow-lambda.yml')
|
||
|
|
expect(extractDeployTarget(yaml)).toEqual({
|
||
|
|
type: 'lambda_function',
|
||
|
|
name: 'payment-webhook-handler'
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
**Mocking strategy:**
|
||
|
|
- Use `nock` or `msw` to intercept GitHub GraphQL API calls
|
||
|
|
- Fixture files in `tests/fixtures/github/` for realistic API responses
|
||
|
|
- Test the GraphQL query builder separately from the HTTP client
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.3 Reconciliation Engine (Node.js / Jest)
|
||
|
|
|
||
|
|
**What to test:**
|
||
|
|
- Cross-referencing AWS resources with GitHub repos (all 5 matching rules)
|
||
|
|
- Deduplication when multiple signals point to the same service
|
||
|
|
- Conflict resolution when signals disagree
|
||
|
|
- Batch processing of SQS messages
|
||
|
|
|
||
|
|
**Key test cases:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
describe('ReconciliationEngine', () => {
|
||
|
|
describe('matchAWSToGitHub', () => {
|
||
|
|
it('explicit_tag_match_takes_highest_priority', () => {
|
||
|
|
const awsService = buildAWSService({ tags: { github_repo: 'acme/payment-gateway' } })
|
||
|
|
const ghRepo = buildGHRepo({ name: 'payment-gateway', org: 'acme' })
|
||
|
|
const result = reconcile([awsService], [ghRepo])
|
||
|
|
expect(result[0].repoLinkSource).toBe('explicit_tag')
|
||
|
|
expect(result[0].repoLinkConfidence).toBe(0.95)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('deploy_workflow_match_used_when_no_explicit_tag', () => {
|
||
|
|
const awsService = buildAWSService({ name: 'payment-api' })
|
||
|
|
const ghRepo = buildGHRepo({ deployTarget: 'payment-api' })
|
||
|
|
const result = reconcile([awsService], [ghRepo])
|
||
|
|
expect(result[0].repoLinkSource).toBe('deploy_workflow')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('fuzzy_name_match_used_as_fallback', () => {
|
||
|
|
const awsService = buildAWSService({ name: 'payment-service' })
|
||
|
|
const ghRepo = buildGHRepo({ name: 'payment-svc' })
|
||
|
|
const result = reconcile([awsService], [ghRepo])
|
||
|
|
expect(result[0].repoLinkSource).toBe('name_match')
|
||
|
|
expect(result[0].repoLinkConfidence).toBe(0.75)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('no_match_produces_aws_only_service_entity', () => {
|
||
|
|
const awsService = buildAWSService({ name: 'legacy-monolith' })
|
||
|
|
const result = reconcile([awsService], [])
|
||
|
|
expect(result[0].repoUrl).toBeNull()
|
||
|
|
expect(result[0].discoverySources).toContain('cloudformation')
|
||
|
|
expect(result[0].discoverySources).not.toContain('github_repo')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('deduplicates_cfn_stack_and_ecs_service_with_same_name', () => {
|
||
|
|
const cfnService = buildAWSService({ source: 'cloudformation', name: 'payment-api' })
|
||
|
|
const ecsService = buildAWSService({ source: 'ecs_service', name: 'payment-api' })
|
||
|
|
const result = reconcile([cfnService, ecsService], [])
|
||
|
|
expect(result).toHaveLength(1)
|
||
|
|
expect(result[0].discoverySources).toContain('cloudformation')
|
||
|
|
expect(result[0].discoverySources).toContain('ecs_service')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.4 Ownership Inference Engine (Python / pytest)
|
||
|
|
|
||
|
|
This is the highest-value unit test target. Ownership inference is the most complex logic and the most likely source of accuracy failures.
|
||
|
|
|
||
|
|
**Key test cases:**
|
||
|
|
|
||
|
|
```python
|
||
|
|
class TestOwnershipScorer:
|
||
|
|
def test_codeowners_weighted_highest_at_0_40(self):
|
||
|
|
signals = [Signal(type='codeowners', team='payments', raw_score=1.0)]
|
||
|
|
result = score_ownership(signals)
|
||
|
|
assert result['payments'].weighted_score == pytest.approx(0.40)
|
||
|
|
|
||
|
|
def test_multiple_signals_summed_correctly(self):
|
||
|
|
signals = [
|
||
|
|
Signal(type='codeowners', team='payments', raw_score=1.0), # 0.40
|
||
|
|
Signal(type='cfn_tag', team='payments', raw_score=1.0), # 0.20
|
||
|
|
Signal(type='git_blame_frequency', team='payments', raw_score=1.0), # 0.25
|
||
|
|
]
|
||
|
|
result = score_ownership(signals)
|
||
|
|
assert result['payments'].total_score == pytest.approx(0.85)
|
||
|
|
|
||
|
|
def test_primary_owner_is_highest_scoring_team(self):
|
||
|
|
signals = [
|
||
|
|
Signal(type='codeowners', team='payments', raw_score=1.0),
|
||
|
|
Signal(type='git_blame_frequency', team='platform', raw_score=1.0),
|
||
|
|
]
|
||
|
|
result = score_ownership(signals)
|
||
|
|
assert result.primary_owner == 'payments'
|
||
|
|
|
||
|
|
def test_service_marked_unowned_when_top_score_below_0_50(self):
|
||
|
|
signals = [Signal(type='git_blame_frequency', team='unknown', raw_score=0.3)]
|
||
|
|
result = score_ownership(signals)
|
||
|
|
assert result.status == 'unowned'
|
||
|
|
|
||
|
|
def test_service_marked_ambiguous_when_top_two_within_0_10(self):
|
||
|
|
signals = [
|
||
|
|
Signal(type='codeowners', team='payments', raw_score=0.8),
|
||
|
|
Signal(type='codeowners', team='platform', raw_score=0.75),
|
||
|
|
]
|
||
|
|
result = score_ownership(signals)
|
||
|
|
assert result.status == 'ambiguous'
|
||
|
|
|
||
|
|
def test_user_correction_overrides_all_inference_with_score_1_00(self):
|
||
|
|
signals = [
|
||
|
|
Signal(type='codeowners', team='payments', raw_score=1.0),
|
||
|
|
Signal(type='user_correction', team='platform', raw_score=1.0),
|
||
|
|
]
|
||
|
|
result = score_ownership(signals)
|
||
|
|
assert result.primary_owner == 'platform'
|
||
|
|
assert result.primary_confidence == 1.00
|
||
|
|
assert result.primary_source == 'user_correction'
|
||
|
|
|
||
|
|
def test_correction_propagation_applies_to_matching_repo_prefix(self):
|
||
|
|
correction = Correction(repo='payment-gateway', team='payments')
|
||
|
|
candidates = ['payment-processor', 'payment-webhook', 'auth-service']
|
||
|
|
propagated = propagate_correction(correction, candidates)
|
||
|
|
assert 'payment-processor' in propagated
|
||
|
|
assert 'payment-webhook' in propagated
|
||
|
|
assert 'auth-service' not in propagated
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.5 Portal API — Route Handlers (Node.js / Jest + Supertest)
|
||
|
|
|
||
|
|
**What to test:**
|
||
|
|
- Tenant isolation enforcement (tenant_id injected into every query)
|
||
|
|
- Search endpoint proxies to Meilisearch with mandatory tenant filter
|
||
|
|
- PATCH /services enforces correction logging
|
||
|
|
- Auth middleware rejects unauthenticated requests
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
describe('GET /api/v1/services/search', () => {
|
||
|
|
it('injects_tenant_id_filter_into_meilisearch_query', async () => {
|
||
|
|
const spy = jest.spyOn(meilisearchClient, 'search')
|
||
|
|
await request(app).get('/api/v1/services/search?q=payment').set('Authorization', `Bearer ${tenantAToken}`)
|
||
|
|
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
|
||
|
|
filter: expect.stringContaining(`tenant_id = '${TENANT_A_ID}'`)
|
||
|
|
}))
|
||
|
|
})
|
||
|
|
|
||
|
|
it('returns_401_when_no_auth_token_provided', async () => {
|
||
|
|
const res = await request(app).get('/api/v1/services/search?q=payment')
|
||
|
|
expect(res.status).toBe(401)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('tenant_a_cannot_see_tenant_b_services', async () => {
|
||
|
|
// Seed Meilisearch with services for both tenants
|
||
|
|
// Query as tenant A, assert no tenant B results
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('PATCH /api/v1/services/:id', () => {
|
||
|
|
it('stores_correction_in_corrections_table', async () => {
|
||
|
|
await request(app)
|
||
|
|
.patch(`/api/v1/services/${SERVICE_ID}`)
|
||
|
|
.send({ team_id: NEW_TEAM_ID })
|
||
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
||
|
|
const correction = await db.corrections.findFirst({ where: { service_id: SERVICE_ID } })
|
||
|
|
expect(correction).toBeDefined()
|
||
|
|
expect(correction.new_value).toMatchObject({ team_id: NEW_TEAM_ID })
|
||
|
|
})
|
||
|
|
|
||
|
|
it('sets_confidence_to_1_00_on_user_correction', async () => {
|
||
|
|
await request(app).patch(`/api/v1/services/${SERVICE_ID}`).send({ team_id: NEW_TEAM_ID })
|
||
|
|
const ownership = await db.service_ownership.findFirst({ where: { service_id: SERVICE_ID } })
|
||
|
|
expect(ownership.confidence).toBe(1.00)
|
||
|
|
expect(ownership.source).toBe('user_correction')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.6 Slack Bot Command Handlers (Node.js / Jest)
|
||
|
|
|
||
|
|
**What to test:**
|
||
|
|
- Command parsing (`/dd0c who owns <service>`)
|
||
|
|
- Typo tolerance matching logic (delegated to search, but bot needs to handle 0 results)
|
||
|
|
- Block kit message formatting
|
||
|
|
- Error handling (unauthorized workspace, missing service)
|
||
|
|
|
||
|
|
### 3.7 Feature Flags & Governance Policy (Node.js / Jest)
|
||
|
|
|
||
|
|
**What to test:**
|
||
|
|
- Flag evaluation (`openfeature` provider)
|
||
|
|
- Governance strict vs. audit mode
|
||
|
|
- Panic mode blocking writes
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Integration Test Strategy
|
||
|
|
|
||
|
|
Integration tests verify that our code correctly interacts with external boundaries: databases, caches, search indices, and third-party APIs.
|
||
|
|
|
||
|
|
### 4.1 Service Boundary Tests
|
||
|
|
- **Discovery ↔ GitHub/GitLab:** Use `nock` or `MSW` to mock the GitHub GraphQL endpoint. Assert that the Node.js scanner constructs the correct query and handles rate limits (HTTP 403/429) via retries.
|
||
|
|
- **Catalog ↔ PostgreSQL:** Use Testcontainers for PostgreSQL to verify complex `upsert` queries, foreign key constraints, and RLS (Row-Level Security) tenant isolation.
|
||
|
|
- **API ↔ Meilisearch:** Use a Meilisearch Docker container. Assert that document syncing (PostgreSQL -> SQS -> Meilisearch) completes and search queries with `tenant_id` filters return the expected subset of data.
|
||
|
|
|
||
|
|
### 4.2 Git Provider API Contract Tests
|
||
|
|
- Write scheduled "contract tests" that run against the *live* GitHub API daily using a dedicated test org.
|
||
|
|
- These detect if GitHub changes their GraphQL schema or rate limit behavior.
|
||
|
|
- Assert that `HEAD:CODEOWNERS` blob extraction still works.
|
||
|
|
|
||
|
|
### 4.3 Testcontainers for Local Infrastructure
|
||
|
|
- **Database:** `testcontainers-node` spinning up `postgres:15-alpine`.
|
||
|
|
- **Search:** `getmeili/meilisearch:latest`.
|
||
|
|
- **Cache:** `redis:7-alpine`.
|
||
|
|
- Run these in GitHub Actions via Docker-in-Docker.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. E2E & Smoke Tests
|
||
|
|
|
||
|
|
E2E tests treat the system as a black box, interacting only through the API and the React UI. We keep these fast and focused on the "5-Minute Miracle" critical path.
|
||
|
|
|
||
|
|
### 5.1 Critical User Journeys (Playwright)
|
||
|
|
1. **The Onboarding Flow:** Mock GitHub OAuth login -> Connect AWS (mock CFN role ARN validation) -> Trigger Discovery -> Wait for WebSocket completion -> Verify 147 services appear in catalog.
|
||
|
|
2. **Cmd+K Search:** Open modal (`Cmd+K`) -> type "pay" -> assert "payment-gateway" is highlighted in < 200ms -> press Enter -> assert service detail card opens.
|
||
|
|
3. **Correcting Ownership:** Open service detail -> Click "Correct Owner" -> select new team -> assert badge changes to 100% confidence -> assert Meilisearch is updated.
|
||
|
|
|
||
|
|
### 5.2 The >80% Auto-Discovery Accuracy Validation
|
||
|
|
- **The "Party Mode" Org:** Maintain a real GitHub org and a mock AWS environment with exactly 100 known services, 10 known teams, and specific chaotic naming conventions.
|
||
|
|
- **The Assertion:** Run discovery. Assert that > 80 of the services are correctly inferred with the right primary owner and repo link.
|
||
|
|
- *This is the most important test in the suite. If a PR drops this below 80%, it cannot be merged.*
|
||
|
|
|
||
|
|
### 5.3 Synthetic Topology Generation
|
||
|
|
- Script to generate `N` mock CFN stacks, `M` ECS services, and `K` GitHub repos to feed the E2E environment without hitting AWS/GitHub limits.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Performance & Load Testing
|
||
|
|
|
||
|
|
Load tests ensure the serverless architecture scales correctly and the Cmd+K search remains instantaneous.
|
||
|
|
|
||
|
|
### 6.1 Discovery Scan Benchmarks
|
||
|
|
- **Target:** 500 AWS resources + 500 GitHub repos scanned and reconciled in < 120 seconds.
|
||
|
|
- **Tooling:** K6 or Artillery. Push 5,000 synthetic SQS messages into the Reconciler queue and measure Lambda batch processing throughput.
|
||
|
|
|
||
|
|
### 6.2 Catalog Query Latency
|
||
|
|
- **Target:** API search endpoint returns in < 100ms at the 99th percentile.
|
||
|
|
- **Test:** Load Meilisearch with 10,000 service documents. Fire 50 concurrent Cmd+K search requests per second. Assert p99 latency.
|
||
|
|
|
||
|
|
### 6.3 Concurrent Scorecard Evaluation
|
||
|
|
- Ensure the Python inference Lambda can evaluate 1,000 services concurrently without database connection exhaustion (using Aurora Serverless v2 connection pooling).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. CI/CD Pipeline Integration
|
||
|
|
|
||
|
|
The test pyramid is enforced through GitHub Actions.
|
||
|
|
|
||
|
|
### 7.1 Test Stages
|
||
|
|
- **Pre-commit:** Husky runs ESLint, Prettier, and fast unit tests (Jest/pytest) for changed files only.
|
||
|
|
- **PR Gate:** Runs the full Unit and Integration test suites. Blocks merge if coverage drops or tests fail.
|
||
|
|
- **Merge (Main):** Deploys to Staging. Runs E2E Critical User Journeys and the 80% Accuracy Validation suite against the Party Mode org.
|
||
|
|
- **Post-Deploy:** Smoke tests verify health endpoints and ALB routing in production.
|
||
|
|
|
||
|
|
### 7.2 Coverage Thresholds
|
||
|
|
- Global Unit Test Coverage: 80%
|
||
|
|
- Ownership Inference & Reconciliation Logic: 95%
|
||
|
|
- Feature Flag & Governance Evaluators: 100%
|
||
|
|
|
||
|
|
### 7.3 Test Parallelization
|
||
|
|
- Jest tests run with `--maxWorkers=50%` locally, `100%` in CI.
|
||
|
|
- Integration tests using Testcontainers run serially per file to avoid database port conflicts, or use dynamic port binding and separate schemas for parallel execution.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. Transparent Factory Tenet Testing
|
||
|
|
|
||
|
|
Testing the governance and compliance features of the IDP itself.
|
||
|
|
|
||
|
|
### 8.1 Feature Flag Circuit Breakers
|
||
|
|
- **Test:** Enable a flagged discovery heuristic that generates 10 phantom services.
|
||
|
|
- **Assert:** The system detects the threshold (>5 unconfirmed), auto-disables the flag, and marks the 10 services as `status: quarantined`.
|
||
|
|
|
||
|
|
### 8.2 Schema Migration Validation
|
||
|
|
- **Test:** Attempt to apply a PR that drops a column from the `services` table.
|
||
|
|
- **Assert:** CI migration validator script fails the build (additive-only rule).
|
||
|
|
|
||
|
|
### 8.3 Decision Log Enforcement
|
||
|
|
- **Test:** Run a discovery scan where service ownership is inferred from `git blame`.
|
||
|
|
- **Assert:** A `decision_log` entry is written to PostgreSQL with the prompt/reasoning, alternatives, and confidence.
|
||
|
|
|
||
|
|
### 8.4 OTEL Span Assertions
|
||
|
|
- **Test:** Run the Reconciler Lambda.
|
||
|
|
- **Assert:** The `catalog_scan` parent span contains child spans for `ownership_inference` with attributes for `catalog.service_id`, `catalog.ownership_signals`, and `catalog.confidence_score`. Use an in-memory OTEL exporter for testing.
|
||
|
|
|
||
|
|
### 8.5 Governance Policy Enforcement
|
||
|
|
- **Test:** Set tenant policy to `strict` mode. Simulate auto-discovery finding a new service.
|
||
|
|
- **Assert:** Service is placed in the "pending review" queue and NOT visible in the main catalog.
|
||
|
|
- **Test:** Set `panic_mode: true`. Attempt a `PATCH /api/v1/services/123`.
|
||
|
|
- **Assert:** HTTP 503 Service Unavailable.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. Test Data & Fixtures
|
||
|
|
|
||
|
|
High-quality fixtures are the lifeblood of this TDD strategy.
|
||
|
|
|
||
|
|
### 9.1 GitHub/GitLab API Response Factories
|
||
|
|
- JSON files containing real obfuscated GraphQL responses for Repositories, `CODEOWNERS` blobs, and Team memberships.
|
||
|
|
- Use factories (e.g., `fishery` or custom functions) to easily override fields: `buildGHRepo({ name: 'auth-service', languages: ['Go'] })`.
|
||
|
|
|
||
|
|
### 9.2 Synthetic Topology Generators
|
||
|
|
- Scripts that generate interconnected AWS resources (e.g., a CFN stack containing an API Gateway routing to 3 Lambdas interacting with 1 RDS instance).
|
||
|
|
|
||
|
|
### 9.3 `CODEOWNERS` and Git Blame Mocks
|
||
|
|
- Diverse `CODEOWNERS` files covering edge cases: wildcard matching, deep path matching, invalid syntax, user-vs-team owners.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. TDD Implementation Order
|
||
|
|
|
||
|
|
To bootstrap the platform efficiently, testing and development should follow this sequence based on Epic dependencies:
|
||
|
|
|
||
|
|
1. **Epic 2 (GitHub Parsers):** Write pure unit tests for `CODEOWNERS` parser and `README` extractor. *Value: High ROI, zero dependencies.*
|
||
|
|
2. **Epic 1 (AWS Heuristics):** Write unit tests for mapping CFN stacks and Tags to Service entities. *Value: Core product logic.*
|
||
|
|
3. **Epic 2 (Ownership Inference):** TDD the scoring algorithm. Build the weighting math. *Value: The brain of the platform.*
|
||
|
|
4. **Epic 3 (Service Catalog Schema):** Integration tests for PostgreSQL RLS and upserting services. *Value: Data durability.*
|
||
|
|
5. **Epic 2 (Reconciliation):** Unit tests merging AWS and GitHub mock entities. *Value: Pipeline glue.*
|
||
|
|
6. **Epic 4 (Search Sync):** Integration tests for pushing DB updates to Meilisearch.
|
||
|
|
7. **Epic 5 (API & UI):** E2E test for the Cmd+K search flow.
|
||
|
|
8. **Epic 10 (Governance & Flags):** Unit tests for feature flag circuit breakers and strict mode.
|
||
|
|
9. **Epic 9 (Onboarding):** Playwright E2E for the 5-Minute Miracle flow.
|
||
|
|
|
||
|
|
This sequence ensures the most complex algorithmic logic is proven before it is wired to databases and APIs.
|