diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 2679523..40ba3de 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -6,6 +6,9 @@ on: push: branches: [main] +permissions: + contents: read + jobs: detect-integration-changes: runs-on: ubuntu-latest @@ -38,20 +41,44 @@ jobs: env: # Compose can be slow on runners (Kafka + topic init + server build) INTEGRATION_READY_TIMEOUT: 120s + # BuildKit + compose build: better layer reuse; GHA cache requires buildx (see below). + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 steps: - name: Checkout repo uses: actions/checkout@v4 + - name: Cache Docker images + uses: ScribeMD/docker-cache@0.5.0 + with: + key: docker-cache-${{ runner.os }}-${{ hashFiles('docker-compose.yml', 'docker-compose.dev.yml', 'testing/integration/docker-compose.integration.yml', 'testing/integration/docker-compose.ci-cache.yml') }} + + # Enables type=gha BuildKit cache used in testing/integration/docker-compose.ci-cache.yml + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod cache: true - # make integration = docker compose up + go test -tags=integration (see testing/Makefile) + - name: Pull and run services + run: | + docker compose \ + --project-directory "${{ github.workspace }}" \ + -f docker-compose.dev.yml \ + -f testing/integration/docker-compose.integration.yml \ + -f testing/integration/docker-compose.ci-cache.yml \ + up -d --build + + # make integration = docker compose up + go test -tags=integration (see testing/Makefile). + # On GitHub Actions, Makefile adds docker-compose.ci-cache.yml for BuildKit GHA layer cache. - name: Run integration tests - run: make integration + run: | + cd testing/integration + go test -tags=integration -count=1 -parallel 8 -v ./... - name: Tear down Compose if: always() diff --git a/testing/Makefile b/testing/Makefile index 3b3c0c6..088e2e3 100644 --- a/testing/Makefile +++ b/testing/Makefile @@ -5,10 +5,18 @@ _TESTING_ABS := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) REPO_ROOT := $(abspath $(_TESTING_ABS)/..) COMPOSE_BASE := $(REPO_ROOT)/docker-compose.dev.yml COMPOSE_INT := $(REPO_ROOT)/testing/integration/docker-compose.integration.yml +COMPOSE_CI_CACHE := $(REPO_ROOT)/testing/integration/docker-compose.ci-cache.yml COMPOSE_LOAD := $(REPO_ROOT)/testing/load/docker-compose.load.yml INTEGRATION_DIR := $(REPO_ROOT)/testing/integration LOAD_DIR := $(REPO_ROOT)/testing/load +# GitHub Actions sets GITHUB_ACTIONS=true — use BuildKit GHA cache (type=gha) for image builds. +ifeq ($(GITHUB_ACTIONS),true) +COMPOSE_EXTRA := -f $(COMPOSE_CI_CACHE) +else +COMPOSE_EXTRA := +endif + # Default to running all integration tests if no filter is provided TEST ?= all @@ -16,7 +24,7 @@ TEST ?= all # Tear down Compose stacks (absolute -f paths; works from any cwd). teardown-integration: - docker compose --project-directory "$(REPO_ROOT)" -f $(COMPOSE_BASE) -f $(COMPOSE_INT) down + docker compose --project-directory "$(REPO_ROOT)" -f $(COMPOSE_BASE) -f $(COMPOSE_INT) $(COMPOSE_EXTRA) down teardown-load: docker compose --project-directory "$(REPO_ROOT)" -f $(COMPOSE_BASE) -f $(COMPOSE_LOAD) down @@ -30,11 +38,11 @@ teardown: teardown-integration teardown-load # make integration TEST=create (event creation flow only) integration: _up_integration ifeq ($(TEST),register) - cd $(INTEGRATION_DIR) && go test -tags=integration -v -run TestRegistration ./... + cd $(INTEGRATION_DIR) && go test -tags=integration -count=1 -parallel 8 -v -run TestRegistration ./... else ifeq ($(TEST),create) - cd $(INTEGRATION_DIR) && go test -tags=integration -v -run TestEventCreation ./... + cd $(INTEGRATION_DIR) && go test -tags=integration -count=1 -parallel 8 -v -run TestEventCreation ./... else - cd $(INTEGRATION_DIR) && go test -tags=integration -v ./... + cd $(INTEGRATION_DIR) && go test -tags=integration -count=1 -parallel 8 -v ./... endif # Flood the registration endpoint with concurrent requests via k6 @@ -49,6 +57,7 @@ _up_integration: --project-directory "$(REPO_ROOT)" \ -f $(COMPOSE_BASE) \ -f $(COMPOSE_INT) \ + $(COMPOSE_EXTRA) \ up -d --build # Spin up the stack with mock Clark for load tests (leaves containers running) diff --git a/testing/integration/creation_test.go b/testing/integration/creation_test.go index 891dc70..b9c7e3a 100644 --- a/testing/integration/creation_test.go +++ b/testing/integration/creation_test.go @@ -12,6 +12,7 @@ import ( // TestEventCreationFlow_CreateAndRetrieve creates an event and verifies it can be // fetched back with matching fields. func TestEventCreationFlow_CreateAndRetrieve(t *testing.T) { + t.Parallel() body := defaultEvent("Create And Retrieve Test", 50) eventID := createEvent(t, "admin-create-retrieve", body) @@ -42,6 +43,7 @@ func TestEventCreationFlow_CreateAndRetrieve(t *testing.T) { // TestEventCreationFlow_Update creates an event, patches the name and location, // then verifies the updated values are returned. func TestEventCreationFlow_Update(t *testing.T) { + t.Parallel() eventID := createEvent(t, "admin-update", defaultEvent("Update Test Original", 50)) patch, _ := json.Marshal(map[string]interface{}{ @@ -83,6 +85,7 @@ func TestEventCreationFlow_Update(t *testing.T) { // TestEventCreationFlow_Delete creates an event, deletes it, and verifies a // subsequent GET returns 404. Only draft or closed events may be deleted (see DeleteEventByID). func TestEventCreationFlow_Delete(t *testing.T) { + t.Parallel() body := defaultEvent("Delete Test", 50) body["status"] = "draft" eventID := createEvent(t, "admin-delete", body) diff --git a/testing/integration/docker-compose.ci-cache.yml b/testing/integration/docker-compose.ci-cache.yml new file mode 100644 index 0000000..8d1f2a2 --- /dev/null +++ b/testing/integration/docker-compose.ci-cache.yml @@ -0,0 +1,16 @@ +# CI-only: enable BuildKit GitHub Actions cache for custom images (see .github/workflows/integration.yml). +# Loaded when CI=true (GitHub Actions sets this automatically). Safe to omit locally. +services: + server: + build: + cache_from: + - type=gha,scope=server + cache_to: + - type=gha,mode=max,scope=server + + mock-clark: + build: + cache_from: + - type=gha,scope=mock-clark + cache_to: + - type=gha,mode=max,scope=mock-clark diff --git a/testing/integration/registration_test.go b/testing/integration/registration_test.go index 735d0d1..2807cb7 100644 --- a/testing/integration/registration_test.go +++ b/testing/integration/registration_test.go @@ -15,6 +15,7 @@ func isRegistrationSubmitted(status int) bool { // TestRegistrationFlow_BasicRegistration creates an event, registers a user, // and verifies the registration is accepted by the Kafka consumer. func TestRegistrationFlow_BasicRegistration(t *testing.T) { + t.Parallel() eventID := createEvent(t, "admin-basic-reg", defaultEvent("Basic Registration Test", 100)) status, requestID := registerForEvent(eventID, "user-basic-reg", "Alice Smith", "alice@example.com") @@ -37,6 +38,7 @@ func TestRegistrationFlow_BasicRegistration(t *testing.T) { // TestRegistrationFlow_DuplicateRegistration verifies that registering the same // user for the same event twice is rejected with 409. func TestRegistrationFlow_DuplicateRegistration(t *testing.T) { + t.Parallel() eventID := createEvent(t, "admin-dup-reg", defaultEvent("Duplicate Registration Test", 100)) // first registration @@ -60,6 +62,7 @@ func TestRegistrationFlow_DuplicateRegistration(t *testing.T) { // TestRegistrationFlow_CapacityFull creates an event with capacity 1, fills it, // then verifies a second user's registration is rejected as capacity_full. func TestRegistrationFlow_CapacityFull(t *testing.T) { + t.Parallel() eventID := createEvent(t, "admin-cap-full", defaultEvent("Capacity Full Test", 1)) // register first user — should be accepted