• English
  • Testing

    Overview

    Tests live in tests/SalonCloud.Tests/ and use xUnit with Testcontainers (real PostgreSQL 16 containers) and WebApplicationFactory (in-memory ASP.NET Core hosts).

    Prerequisites

    • Docker (Testcontainers pulls Postgres 16)
    • .NET 10 SDK

    Running tests

    # All tests (unit + integration)
    dotnet test
    
    # With filter (example)
    dotnet test --filter "FullyQualifiedName~SyncBatch"
    
    # Release build
    dotnet test --configuration Release

    Test structure

    tests/SalonCloud.Tests/
    ├── Integration/          ~230 test files — component/end-to-end tests
    ├── Unit/                 ~65 test files — pure unit tests
    ├── Infra/                1 file — CDK synth assertions
    └── Fixtures/             1 file — sample_batch.json test data

    The test project references all service projects. Web projects are aliased to avoid name collisions (e.g., AccountsWeb, BookingWeb).

    Integration test infrastructure

    PostgresFixture

    PostgresFixture.cs implements IAsyncLifetime and manages a single Testcontainers Postgres 16 instance shared across all integration tests:

    • max_connections=1000 — needed because ~300+ WebApplicationFactory instances accumulate
    • MinPoolSize=1 to keep peak idle connections manageable
    • Runs EF migrations for all service DbContexts before any test runs
    • Sets up Observability TCP stub so Accounts onboard tests don't burn retry budget
    • Sets Otp__MaxIssuesPerWindow=10000 to prevent throttle exhaustion
    • Disables HealthSnapshot and RuleEngine by default

    Collection fixture pattern

    Integration tests use xUnit collection fixtures:

    [Collection("postgres")]
    public class SyncBatchEndpointTests(PostgresFixture fx) : IClassFixture<WebApplicationFactory<Program>>
    {
        private WebApplicationFactory<Program> Factory() =>
            new WebApplicationFactory<Program>()
                .WithWebHostBuilder(b => b.ConfigureServices(s =>
                {
                    s.RemoveAll<NpgsqlDataSourceProvider>();
                    s.AddSingleton(new NpgsqlDataSourceProvider(fx.AdminConnectionString));
                }));
    
        [Fact]
        public async Task Happy_path_returns_ok_true_and_lands()
        {
            var store = Guid.NewGuid();
            await fx.SeedActiveStoreAsync(store);
            using var factory = Factory();
            using var client = factory.CreateClient();
            var resp = await client.PostAsJsonAsync("/v1/sync/batch", Batch(store, ...));
            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var ack = await resp.Content.ReadFromJsonAsync<AckDto>();
            Assert.True(ack!.Ok);
        }
    }

    Key pattern: each test creates a fresh WebApplicationFactory (fresh DI container, fresh in-memory host), but all share the same Postgres container via the collection fixture.

    What's tested (integration)

    • Sync batch: happy path, NUL-byte rejection, duplicate batch idempotency, suppression detection, clock-skew clamping, lease conflicts, unauthorized/forbidden, sequentia restore
    • Booking: availability, holds, OTP flow, booking create/cancel/reschedule, reconciliation, outbox pull/ack, rate limiting
    • Auth: login/logout, OTP verify, session refresh, token validation
    • Onboarding: self-onboard, store activation/deactivation
    • Device registration: device bind, device revoke
    • Operator admin: role CRUD, operator CRUD, permission management, audit log
    • Entitlement management: per-store override, feature catalog
    • Notification dispatch: booking confirmations, reminders
    • SMS webhooks: inbound/status callbacks (Twilio)
    • DataHub: RLS enforcement, PII hardening, view correctness, schema migrations
    • Capability convergence: poller applies all pages, cursor advances correctly
    • Media: image upload validation, S3 put
    • CDK synth: VPC structure, RDS properties, WAF rules, ALB path routing, Service Connect wiring

    Unit tests

    Pure unit tests (no database, no DI container) cover:

    CategoryExamples
    Core logicSlotCalculator, BatchValidator, HealthScorer, ProofOfWork, MetricsParser
    SecurityOtpHasher, OtpPepper, TokenHasher, SecretComparer, HttpHardening
    DataPhoneNumber, ContactNormalizer, StoreLocalTime, ImageValidator
    InfrastructureS3MediaStorage, ConnectionPooling, TwilioSignature
    AdminFeatureCatalog, PermissionCatalog

    CDK synth tests

    tests/SalonCloud.Tests/Infra/CdkSynthTests.cs — synthesizes CloudFormation templates and asserts on:

    • VPC: 2 AZs, subnets, NAT gateway count
    • RDS: engine version, instance class, deletion protection
    • WAF: rule presence
    • ALB: path-based routing rules, /internal/* blocking
    • Service Connect: namespace, service entries

    Test frameworks and dependencies

    DependencyVersionPurpose
    xunit2.9.3Test framework
    xunit.runner.visualstudio3.1.4Test runner
    Testcontainers.PostgreSql4.12.0Real Postgres containers
    Microsoft.AspNetCore.Mvc.Testing10.0.8In-memory host testing
    coverlet.collector6.0.4Code coverage
    Amazon.CDK.AssertionsCloudFormation assertions

    When adding new tests

    • Integration: Use [Collection("postgres")] to share the Postgres container. Create a fresh WebApplicationFactory per test. Use PostgresFixture helper methods to seed prerequisite state.
    • Unit: No DB, no DI. Test pure functions in isolation. Follow the existing pattern of class-per-test-subject.
    • CDK: Add to CdkSynthTests.cs if testing infrastructure shape, or create new test class for new stack assertions.

    Remember: DataHub is a migration-only project with no web host — its tests verify SQL views and RLS via direct database queries from integration tests.

    More on the architecture these tests exercise in Architecture and Data Flow.