Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
30 changes: 26 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,32 @@ 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. Each helper is `sync.Once`-shared per test process:
- `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; use a per-test unique index name (fnv hash of `t.Name()`)
- `testutil.NATS(t) string` — shared NATS URL with JetStream enabled
- `testutil.Valkey(t) string` — shared Valkey addr; call `testutil.FlushValkey(t)` in cleanup
- **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 packages that want concurrent pre-warming, wrap manually instead — see `search-service/setup_shared_test.go` for the reference pattern (`EnsureXxx` goroutines + error channel + fail-fast).
- **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 Valkey call `FlushValkey` on cleanup; for NATS use a per-test `*nats.Conn` pair with `Drain`/`Shutdown` cleanups.
- 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) }
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