Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env.dev.example

This file was deleted.

File renamed without changes.
2 changes: 0 additions & 2 deletions .env.test.example

This file was deleted.

27 changes: 4 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,8 @@ jobs:
quality:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_DB: reservation_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

steps:
- uses: actions/checkout@v4
Expand All @@ -44,12 +32,5 @@ jobs:
- name: Typecheck
run: make typecheck

- name: Apply Drizzle migrations
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/reservation_db
run: make dev-migrate

- name: Tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/reservation_db
run: make test
- name: Integration Tests
run: make ci-test
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ lerna-debug.log*
!.vscode/launch.json
!.vscode/extensions.json

# dotenv environment variable files
# dotenv environment variable file
.env
.env.*
!.env.*.example

# temp directory
.temp
Expand Down
69 changes: 28 additions & 41 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,37 @@
# Variáveis
# ─────────────────────────────────────────────────────────────

DEV_ENV = .env.dev
PROD_ENV = .env.prod
TEST_ENV = .env.test

DEV_COMPOSE = docker/docker-compose.dev.yml
PROD_COMPOSE = docker/docker-compose.prod.yml
TEST_COMPOSE = docker/docker-compose.test.yml

MIGRATIONS_DIR = migrations
TREE_IGNORE = node_modules|.git|dist|*.env*

# ─────────────────────────────────────────────────────────────
# Phony
# ─────────────────────────────────────────────────────────────

.PHONY: \
dev-up dev-down dev-reset dev-migrate \
prod-up prod-migrate \
test test-up test-down test-reset test-migrate test-run \
check-env \
lint typecheck build \
check-dev-env check-prod-env check-test-env \
ci-test \
dev-up dev-stop dev-start dev-down dev-reset dev-migrate \
prod-up prod-migrate \
test-up test-down test-reset test-migrate test \
tree

# ─────────────────────────────────────────────────────────────
# Checks
# Environment Check
# ─────────────────────────────────────────────────────────────

check-dev-env:
@test -f $(DEV_ENV) || (echo "❌ .env.dev não existe" && exit 1)
@grep -q "^DATABASE_URL=" $(DEV_ENV) || \
(echo "❌ DATABASE_URL ausente (.env.dev)" && exit 1)

check-prod-env:
@test -f $(PROD_ENV) || (echo "❌ .env.prod não existe" && exit 1)
@grep -q "^DATABASE_URL=" $(PROD_ENV) || \
(echo "❌ DATABASE_URL ausente (.env.prod)" && exit 1)

check-test-env:
@test -f $(TEST_ENV) || (echo "❌ .env.test não existe" && exit 1)
@grep -q "^DATABASE_URL=" $(TEST_ENV) || \
(echo "❌ DATABASE_URL ausente (.env.test)" && exit 1)
check-env:
@if [ -z "$$DATABASE_URL" ]; then \
echo "❌ DATABASE_URL não definida"; \
exit 1; \
fi

# ─────────────────────────────────────────────────────────────
# CI
# Quality
# ─────────────────────────────────────────────────────────────

lint:
Expand All @@ -54,6 +41,10 @@ lint:
typecheck:
pnpm tsc --noEmit

# ─────────────────────────────────────────────────────────────
# CI
# ─────────────────────────────────────────────────────────────

ci-test:
make test-reset
make test-up
Expand All @@ -65,7 +56,7 @@ ci-test:
# DEV
# ─────────────────────────────────────────────────────────────

dev-up: check-dev-env
dev-up:
@echo "🚧 Subindo Postgres DEV"
docker compose -f $(DEV_COMPOSE) up -d

Expand All @@ -85,27 +76,25 @@ dev-reset:
@echo "💥 Resetando ambiente DEV (containers + volumes)"
docker compose -f $(DEV_COMPOSE) down -v

dev-migrate:
npx dotenv-cli -e $(DEV_ENV) -- \
npx ts-node src/infrastructure/scripts/migrate.ts
dev-migrate: check-env
npx ts-node src/infrastructure/scripts/migrate.ts

# ─────────────────────────────────────────────────────────────
# PROD
# ─────────────────────────────────────────────────────────────

prod-up: check-prod-env
prod-up:
@echo "🚀 Subindo Postgres PROD"
docker compose -f $(PROD_COMPOSE) up -d

prod-migrate: check-prod-env
npx dotenv-cli -e $(PROD_ENV) -- \
npx ts-node src/infrastructure/scripts/migrate.ts
prod-migrate: check-env
npx ts-node src/infrastructure/scripts/migrate.ts

# ────────────────────────────────────────────────────────────
# ────────────────────────────────────────────────────────────
# TEST
# ────────────────────────────────────────────────────────────
# ────────────────────────────────────────────────────────────

test-up: check-test-env
test-up:
@echo "🧪 Subindo Postgres TEST"
docker compose -f $(TEST_COMPOSE) up -d

Expand All @@ -117,13 +106,11 @@ test-reset:
@echo "💥 Resetando ambiente TEST (containers + volumes)"
docker compose -f $(TEST_COMPOSE) down -v

test-migrate: check-test-env
npx dotenv-cli -e $(TEST_ENV) -- \
npx ts-node src/infrastructure/scripts/migrate.ts
test-migrate: check-env
npx ts-node src/infrastructure/scripts/migrate.ts

test:
npx dotenv-cli -e $(TEST_ENV) -- \
pnpm jest
pnpm jest

# ─────────────────────────────────────────────────────────────
# Utils
Expand Down
2 changes: 0 additions & 2 deletions docker/docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.9"

services:
postgres_test:
image: postgres:16
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0009-postgres-error-mapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Drizzle ORM wraps PostgreSQL errors, requiring custom logic to extract the under

2. **Repositories will depend only on semantic error checks**

* Example: `PostgresErrorMapper.isUniqueViolation(error)`
* Example: `PostgresErrorMapper.isExclusionViolation(error)`
* Repositories will no longer parse error objects manually.

3. **The mapper remains an infrastructure concern**
Expand Down
145 changes: 145 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
## Testing Strategy

This project adopts a **multi-layered testing strategy** focused on enforcing domain invariants, validating persistence guarantees, and ensuring correct behavior under real database constraints.

The goal is not only to test isolated logic, but to verify that **architectural boundaries and database-level invariants hold under realistic conditions**, including concurrent execution.

---

## Test File Naming Conventions

| Suffix | Meaning | Scope |
| -------------- | ----------------- | -------------------------------------- |
| `.spec.ts` | Unit tests | Domain / pure logic |
| `.int.spec.ts` | Integration tests | Database, repositories, infrastructure |
| `.e2e-spec.ts` | End-to-end tests | HTTP / NestJS application layer |

Test folders mirror bounded contexts (e.g., `test/reservation`, `test/user`).

---

## Test Types

### Unit Tests (Domain Layer)

Unit tests validate pure domain behavior without infrastructure dependencies.

**Targets:**

* Entities
* Value objects
* Domain services
* Logical invariants (e.g., reservation time validity)

**Characteristics:**

* No database
* No NestJS container
* No IO
* Deterministic and fast

These tests validate business rules independently from persistence concerns.

---

### Integration Tests (Persistence & Infrastructure)

Integration tests validate interaction with a real PostgreSQL database.

**Targets:**

* Repository implementations
* Database constraints (UNIQUE, EXCLUDE, FK)
* Error mapping (PostgreSQL → domain errors)
* Schema behavior and migrations

**Characteristics:**

* Uses isolated PostgreSQL container via Docker
* Schema migrations executed before the test suite
* Database truncated before each test for deterministic isolation
* No mocks for persistence layer
* Enforced in CI

Integration tests intentionally rely on real database behavior rather than mocks.
Database constraints are treated as part of the system’s correctness model.

---

### Concurrency Tests

Concurrency tests validate that race conditions are prevented under parallel execution.

**Key invariants tested:**

* A barber cannot have overlapping reservations
* Only one concurrent reservation for the same time slot can succeed

**Strategy:**

* Multiple concurrent transactions are started
* Execution is synchronized using a barrier to ensure simultaneous writes
* `Promise.allSettled` is used to observe outcomes
* PostgreSQL `EXCLUDE USING gist` constraint enforces correctness
* Constraint violations are mapped to `ReservationConflictError`

These tests validate that concurrency protection is enforced at the database level and correctly surfaced at the domain layer.

---

## Invariant Enforcement Model

Invariant enforcement is intentionally split across layers:

* **Domain layer** validates logical invariants (e.g., time range validity).
* **Database layer** enforces structural and concurrency invariants (e.g., overlapping reservations).
* **Repositories** translate database constraint violations into domain-specific errors.

This ensures correctness even under concurrent execution and prevents reliance on application-level checks alone.

---

## Database Test Lifecycle

Integration tests rely on a deterministic PostgreSQL environment:

1. Docker PostgreSQL container for tests
2. SQL migrations executed before the test suite
3. Tables truncated (with cascade) before each test
4. Isolated connection pool for test execution

This guarantees test isolation and reproducibility.

---

## Makefile Test Commands

| Command | Description |
| ------------------- | ------------------------------- |
| `make test-up` | Start PostgreSQL test container |
| `make test-reset` | Drop and recreate schema |
| `make test-migrate` | Execute SQL migrations |
| `make test` | Run Jest test suite |

---

## End-to-End Tests

E2E tests validate HTTP behavior through the NestJS application layer.

**Targets:**

* Controllers
* Use cases
* Serialization
* HTTP error mapping

These tests verify that domain and infrastructure layers integrate correctly at the application boundary.

---

## Future Improvements

* Property-based testing for reservation time-slot edge cases
* High-load concurrency stress testing
* CI-level performance thresholds for critical paths
6 changes: 3 additions & 3 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Config } from 'jest';

const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
globalSetup: '<rootDir>/test/setup/global-setup.ts',
maxWorkers: 1,
moduleNameMapper: {
'^@domain/(.*)$': '<rootDir>/src/domain/$1',
'^@application/(.*)$': '<rootDir>/src/application/$1',
'^@infrastructure/(.*)$': '<rootDir>/src/infrastructure/$1',
'^@http/(.*)$': '<rootDir>/src/http/$1',
'^@test/(.*)$': '<rootDir>/test/$1',
},
preset: 'ts-jest',
testEnvironment: 'node',
};

export default config;
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"@types/pg": "^8.16.0",
"@types/supertest": "^6.0.2",
"dotenv": "^17.2.3",
"dotenv-cli": "^11.0.0",
"drizzle-kit": "^0.31.8",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
Expand Down
Loading
Loading