Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
16c3073
test(search-service): share NATS / ES / Valkey containers across inte…
claude May 20, 2026
4252c54
test(search-service): fail tests when shared Valkey FLUSHDB cleanup e…
claude May 20, 2026
6b23294
test(search-service): split CCS integration tests into their own file
claude May 20, 2026
24e9280
test(search-service): split integration tests by search endpoint
claude May 20, 2026
17ef382
test(search-service): add router cleanup to CCS fixture; trim comments
claude May 20, 2026
1e5d970
test(search-service): pre-warm shared containers in parallel; halve E…
claude May 20, 2026
a71664b
test(search-service): explicit shared-container cleanup; disable Ryuk…
claude May 20, 2026
436dc39
test(testutil,search-service): add TerminateMongo for explicit cleanup
claude May 20, 2026
a5a5c5a
test(testutil): add Elasticsearch, NATS, Valkey + Terminate{All,Xxx}
claude May 20, 2026
08ba0b4
test(testutil): disable Ryuk repo-wide via init(); drop CI env var
claude May 20, 2026
d356c6a
test(repo): migrate every integration package to testutil shared cont…
claude May 20, 2026
66cecfe
test(loadgen): migrate members_integration_test.go to testutil.NATS
claude May 20, 2026
94827bc
test(testutil): re-disable Ryuk via init(); audit per-container cleanup
claude May 20, 2026
cda13a2
test(testutil,search-service): simplify pass — RunTests helper, const…
claude May 20, 2026
713d5eb
test(search-sync-worker): fail-fast on prewarm errors
claude May 20, 2026
1cdad60
test: switch integration test files to internal package per CLAUDE.md
claude May 20, 2026
ff2312a
test(roomkeystore): add missing TestMain
claude May 20, 2026
42552f7
docs(CLAUDE.md): document the testcontainer best practice this PR est…
claude May 20, 2026
ff03e8d
Merge origin/main into testcontainer-share branch
claude May 21, 2026
7a6cc36
Post-merge fixes for Valkey cluster-mode migration
claude May 21, 2026
80514d1
test(testutil): add SharedValkeyCluster + FlushValkey for shared cluster
claude May 21, 2026
c6424e3
chore(history-service): remove local docker-compose folder
claude May 21, 2026
7b76258
test: trim comments per simplify pass
claude May 21, 2026
e757c42
test(testutil): extract PrewarmFailFast, RunTestsWithPrewarm, Elastic…
claude May 21, 2026
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
32 changes: 28 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,34 @@ All commands are wrapped in the root Makefile. Always use `make` targets — nev
- Every exported function in `pkg/` must have corresponding test cases

### Integration Tests
- All integration tests use `//go:build integration` build tag
- Use `testcontainers-go` with official modules (`mongodb`, `cassandra`, `nats`) for real dependencies
- Write `setup<Dep>(t *testing.T)` helpers that start a container, register `t.Cleanup`, and return a connected client
- Use `<service>_test` as database name to avoid collisions
- All integration tests use the `//go:build integration` build tag
- Test files live in the same package as the code under test (`package main` for services, `package <pkg>` for libraries) — never external `*_test` packages
- **Containers come from `pkg/testutil`** — do not start your own with `testcontainers.GenericContainer` / `natsmod.Run` / `mongodb.Run` etc. Process-shared helpers (one container, many tests, started via `sync.Once`, terminated via `TerminateAll`):
- `testutil.MongoDB(t, prefix) *mongo.Database` — isolated DB per test
- `testutil.CassandraKeyspace(t, prefix) (keyspace, *gocql.Session, host)` — isolated keyspace per test
- `testutil.MinIO(t, prefix) (*minio.Client, bucket)` — isolated bucket per test
- `testutil.Elasticsearch(t) string` — shared ES URL; pair with `testutil.ElasticsearchIndex(t, prefix)` for a per-test isolated index (DELETEd on cleanup)
- `testutil.NATS(t) string` — shared NATS URL with JetStream enabled
- Valkey (cluster-mode — services use this in production):
- `testutil.SharedValkeyCluster(t) *redis.ClusterClient` — process-shared cluster (started via `sync.Once`, reaped via `TerminateValkey`/`TerminateAll`). Per-test caller MUST register `t.Cleanup(func() { testutil.FlushValkey(t) })` so sibling tests start with a clean keyspace. Default choice.
- `testutil.StartValkeyCluster(t) *redis.ClusterClient` — per-test cluster (each test gets its own container via `t.Cleanup`). Use ONLY when the test asserts on cluster-routing state (e.g., `pkg/roomkeystore`'s `CLUSTER KEYSLOT` checks) or owns a store wrapper that calls `Close()` on the underlying client.
- **Every integration test package must have a `TestMain` that drives cleanup**:
```go
//go:build integration
package mypkg

import (
"testing"
"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
```
`testutil.RunTests` wraps `m.Run()` + `testutil.TerminateAll()` + `os.Exit(code)`. For concurrent pre-warming use `testutil.RunTestsWithPrewarm(m, testutil.EnsureElasticsearch, testutil.EnsureNATS, ...)` — runs each `EnsureXxx` concurrently and fails fast on the first error before `m.Run`. The `testutil.PrewarmFailFast(fns...)` building block is also exposed for packages that need extra cleanup between `m.Run` and `os.Exit`.
- **Ryuk is disabled repo-wide** (via `pkg/testutil/init.go`) because our CI runner can't run the reaper sidecar. `testutil.TerminateAll` is the only cleanup mechanism on clean exits. SIGKILL / Ctrl+C will leak containers locally — acceptable trade-off; flip Ryuk back on with `TESTCONTAINERS_RYUK_DISABLED=false go test ...` if debugging a leak.
- Per-test isolation is the caller's responsibility: the `MongoDB`/`Cassandra`/`MinIO` helpers already hash `t.Name()`; for ES use a per-test unique index name and DELETE on cleanup; for NATS use a per-test `*nats.Conn` pair with `Drain`/`Shutdown` cleanups; for shared Valkey call `testutil.FlushValkey(t)` in `t.Cleanup` (StartValkeyCluster's per-test mode is automatic).
- Inline `testcontainers.GenericContainer` is only acceptable when a shared testutil container can't accommodate the test (e.g. search-service CCS needs two ES nodes on a shared docker network; `pkg/roomkeysender` needs NATS with WebSocket transport; `pkg/roomcrypto` needs a Node container with bundled scripts). Each inline container must store its reference and register `t.Cleanup(container.Terminate)`.
- New shared dependencies (a container type used by ≥2 packages) belong in `pkg/testutil` with the same shape: `Xxx(t)` + `EnsureXxx()` + `TerminateXxx()`, container ref stored at package level, and `TerminateXxx` wired into `TerminateAll`.

### Model Tests
- `pkg/model/model_test.go` verifies all domain types marshal/unmarshal correctly via a generic `roundTrip` helper
Expand Down
11 changes: 11 additions & 0 deletions broadcast-worker/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package main

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
14 changes: 0 additions & 14 deletions history-service/docker-local/.env.example

This file was deleted.

152 changes: 0 additions & 152 deletions history-service/docker-local/docker-compose.yml

This file was deleted.

11 changes: 11 additions & 0 deletions history-service/internal/cassrepo/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package cassrepo

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
11 changes: 11 additions & 0 deletions history-service/internal/mongorepo/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package mongorepo

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
9 changes: 4 additions & 5 deletions history-service/internal/service/integration_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build integration

package service_test
package service

import (
"context"
Expand All @@ -16,7 +16,6 @@ import (

"github.com/hmchangw/chat/history-service/internal/cassrepo"
"github.com/hmchangw/chat/history-service/internal/models"
"github.com/hmchangw/chat/history-service/internal/service"
"github.com/hmchangw/chat/pkg/model"
"github.com/hmchangw/chat/pkg/msgbucket"
"github.com/hmchangw/chat/pkg/natsrouter"
Expand Down Expand Up @@ -139,7 +138,7 @@ func TestEditMessage_Integration(t *testing.T) {
session := setupCassandra(t)
repo := cassrepo.NewRepository(session, msgbucket.New(24*time.Hour), 365)
pub := &recordingPublisher{}
svc := service.New(repo, alwaysSubscribedRepo{}, stubRoomRepo{}, pub, nil, 730*24*time.Hour)
svc := New(repo, alwaysSubscribedRepo{}, stubRoomRepo{}, pub, nil, 730*24*time.Hour)

sender := models.Participant{ID: "u1", Account: "alice"}
roomID := "r-integ"
Expand Down Expand Up @@ -202,7 +201,7 @@ func TestDeleteMessage_Integration(t *testing.T) {
session := setupCassandra(t)
repo := cassrepo.NewRepository(session, msgbucket.New(24*time.Hour), 365)
pub := &recordingPublisher{}
svc := service.New(repo, alwaysSubscribedRepo{}, stubRoomRepo{}, pub, nil, 730*24*time.Hour)
svc := New(repo, alwaysSubscribedRepo{}, stubRoomRepo{}, pub, nil, 730*24*time.Hour)

sender := models.Participant{ID: "u1", Account: "alice"}
roomID := "r-del-integ"
Expand Down Expand Up @@ -262,7 +261,7 @@ func TestDeleteMessage_ParentWithReplies_NoCascade(t *testing.T) {
session := setupCassandra(t)
repo := cassrepo.NewRepository(session, msgbucket.New(24*time.Hour), 365)
pub := &recordingPublisher{}
svc := service.New(repo, alwaysSubscribedRepo{}, stubRoomRepo{}, pub, nil, 730*24*time.Hour)
svc := New(repo, alwaysSubscribedRepo{}, stubRoomRepo{}, pub, nil, 730*24*time.Hour)

sender := models.Participant{ID: "u1", Account: "alice"}
roomID := "r-parent-cascade"
Expand Down
11 changes: 11 additions & 0 deletions history-service/internal/service/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package service

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
15 changes: 3 additions & 12 deletions inbox-worker/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import (
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
natsmod "github.com/testcontainers/testcontainers-go/modules/nats"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"

"github.com/hmchangw/chat/pkg/model"
"github.com/hmchangw/chat/pkg/stream"
"github.com/hmchangw/chat/pkg/subject"
"github.com/hmchangw/chat/pkg/testutil"
"github.com/hmchangw/chat/pkg/testutil/testimages"
)

func setupMongo(t *testing.T) *mongo.Database {
Expand Down Expand Up @@ -577,20 +575,13 @@ func TestHandleMemberAdded_DM_PersistsRemoteCounterpartSub(t *testing.T) {
assert.False(t, bobSub.IsSubscribed, "DM does not set IsSubscribed=true")
}

// setupNATS starts a NATS container with JetStream enabled and returns a
// JetStream client tied to the test's lifetime.
// setupNATS connects to the process-shared NATS (JetStream enabled in
// testutil) and returns a JetStream client tied to the test's lifetime.
func setupNATS(t *testing.T) (context.Context, jetstream.JetStream) {
t.Helper()
ctx := context.Background()

c, err := natsmod.Run(ctx, testimages.NATS)
require.NoError(t, err)
t.Cleanup(func() { _ = c.Terminate(ctx) })

url, err := c.ConnectionString(ctx)
require.NoError(t, err)

nc, err := nats.Connect(url)
nc, err := nats.Connect(testutil.NATS(t))
require.NoError(t, err)
t.Cleanup(func() { nc.Close() })

Expand Down
11 changes: 11 additions & 0 deletions inbox-worker/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package main

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
11 changes: 11 additions & 0 deletions message-worker/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package main

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
11 changes: 11 additions & 0 deletions notification-worker/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package main

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
11 changes: 11 additions & 0 deletions pkg/minioutil/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package minioutil

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
11 changes: 11 additions & 0 deletions pkg/mongoutil/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build integration

package mongoutil

import (
"testing"

"github.com/hmchangw/chat/pkg/testutil"
)

func TestMain(m *testing.M) { testutil.RunTests(m) }
Loading
Loading