Stage 7 · Master
Phase 6 — Flat Service
Testing
Unit tests for rules, integration tests for constraints
What Each Test Level Can and Cannot Prove
Organization Service's unit-testing and integration-testing lessons own hand-written fakes, build tags, and Testcontainers wiring; User Service already re-applied those mechanics. The flat-specific question is narrower: which inventory invariants can only be proven by a test, not by a type?
Unit Tests: Service Logic Against a Fake Repository
package service_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/hoa-platform/backend/services/flat-service/internal/domain"
"github.com/hoa-platform/backend/services/flat-service/internal/service"
)
type recordingFlatRepo struct{ created domain.Flat }
func (r *recordingFlatRepo) Create(ctx context.Context, flat domain.Flat) (domain.Flat, error) {
r.created = flat
flat.ID = uuid.New()
return flat, nil
}
func (r *recordingFlatRepo) Get(ctx context.Context, tenantID, id uuid.UUID) (domain.Flat, error) {
return domain.Flat{}, domain.ErrFlatNotFound
}
func (r *recordingFlatRepo) List(ctx context.Context, tenantID uuid.UUID, limit, offset int) ([]domain.Flat, error) {
return nil, nil
}
func (r *recordingFlatRepo) Update(ctx context.Context, tenantID, id uuid.UUID, status domain.FlatStatus) (domain.Flat, error) {
return domain.Flat{}, nil
}
func (r *recordingFlatRepo) Delete(ctx context.Context, tenantID, id uuid.UUID) error { return nil }
type acceptingBuildingChecker struct{}
func (acceptingBuildingChecker) Exists(ctx context.Context, tenantID, id uuid.UUID) (bool, error) {
return true, nil
}
func TestFlatService_Create_ForcesVacantStatus(t *testing.T) {
repo := &recordingFlatRepo{}
svc := service.NewFlatService(repo, acceptingBuildingChecker{})
_, err := svc.Create(context.Background(), domain.Flat{
TenantID: uuid.New(),
BuildingID: uuid.New(),
UnitNumber: "A-101",
Status: domain.FlatStatusOccupied,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if repo.created.Status != domain.FlatStatusVacant {
t.Fatalf("new flats must start vacant, got %q", repo.created.Status)
}
}A string type cannot stop a caller from sending occupied on create. The test proves FlatService overwrites that caller-controlled field before persistence.
Integration Tests: Constraints Against Real Postgres
func TestFlatRepository_Create_RejectsDuplicateUnitNumberInBuilding(t *testing.T) {
h := newFlatRepositoryHarness(t)
buildingID := h.seedBuilding(uuid.New())
tenantID := uuid.New()
first := domain.Flat{
TenantID: tenantID, BuildingID: buildingID,
UnitNumber: "A-101", FloorNumber: 1, AreaSqft: 650,
}
if _, err := h.repo.Create(context.Background(), first); err != nil {
t.Fatalf("first create should succeed: %v", err)
}
_, err := h.repo.Create(context.Background(), first)
if err != domain.ErrDuplicateUnitNumber {
t.Fatalf("expected ErrDuplicateUnitNumber, got %v", err)
}
}
func TestFlatRepository_Get_DoesNotLeakAcrossTenants(t *testing.T) {
h := newFlatRepositoryHarness(t)
tenantA, tenantB := uuid.New(), uuid.New()
buildingA := h.seedBuilding(tenantA)
created, err := h.repo.Create(context.Background(), domain.Flat{
TenantID: tenantA, BuildingID: buildingA,
UnitNumber: "A-101", FloorNumber: 1, AreaSqft: 650,
})
if err != nil {
t.Fatalf("setup create failed: %v", err)
}
_, err = h.repo.Get(context.Background(), tenantB, created.ID)
if err != domain.ErrFlatNotFound {
t.Fatalf("expected ErrFlatNotFound for cross-tenant read, got %v", err)
}
}The harness is the same migration-backed Postgres harness introduced in Organization Service's integration-testing lesson; only the flat invariants are shown here.
Invariant Test Commands
uuid.UUID proves shape, not ownership. The cross-tenant read test is valuable because it exercises the repository predicate that a type checker cannot see.
Applied exercise
Prove the FK-based tenant check with a failing test first
Migration 0003's BuildingRepository.Exists tenant filter has no integration test yet — the earlier lesson only described the risk in prose.
- Write TestFlatService_Create_RejectsCrossTenantBuildingID against a real Postgres: seed a building under tenant A, attempt to create a flat under tenant B referencing that building_id.
- Assert the error is domain.ErrBuildingNotFound, not a raw foreign key violation.
- Temporarily remove the tenant_id filter from BuildingRepository.Exists and confirm the test fails — then restore it.
Deliverable
A new integration test file plus a one-line note in the PR description showing the test failing without the fix.
Completion checks
- The test fails when the tenant filter is removed, proving it exercises the real risk.
- The test passes against the current implementation.
- No production code changed — only the test was added.
Which flat invariant belongs in the service unit test rather than the repository integration test?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.