//go:build dbtest // PostgresStore(user)的真 DB 整合測試(DB-on FK 收尾,問題 #1)。 // // build tag `dbtest`:只在帶 `-tags=dbtest` 時編譯/執行(需要 Docker / testcontainers)。 // 預設 `go test ./...`(無 Docker)不會觸碰本檔,維持綠燈。 // // 執行: // // go test -tags=dbtest ./internal/user/... // # 無本機 Docker 時,Orchestrator 在 130 補跑: // DOCKER_HOST=tcp://192.168.0.130:2375 TESTCONTAINERS_RYUK_DISABLED=true \ // go test -tags=dbtest ./internal/user/... // // 涵蓋: // - Upsert 新建 + Get round-trip(id=sub UUID、email、name、roles) // - Upsert 同 id 再寫:更新 email/name/roles、保留 created_at // - email 大小寫不敏感唯一(uq_users_email_lower):兩個不同 sub 共用同 email(差大小寫)→ 撞 unique // - Get 不存在 → ErrNotFound // - roles nil → 寫空陣列(NOT NULL DEFAULT '{}')round-trip 回空 slice package user import ( "context" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgconn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "visiona-backend/internal/db/testsupport" ) // newPGUserStore 啟動一次性測試 DB、truncate users,回傳 store。 func newPGUserStore(t *testing.T) (*PostgresStore, *testsupport.TestDB) { t.Helper() tdb := testsupport.SetupTestDB(t) tdb.Truncate(t, "users") return NewPostgresStore(tdb.Pool), tdb } func TestPGUser_UpsertAndGet(t *testing.T) { ctx := context.Background() s, _ := newPGUserStore(t) id := uuid.NewString() // 模擬 OIDC sub(UUID 格式,D1-B) in := &User{ID: id, Email: "Alice@Example.com", Name: "Alice", Roles: []string{"admin", "user"}} require.NoError(t, s.Upsert(ctx, in)) got, err := s.Get(ctx, id) require.NoError(t, err) assert.Equal(t, id, got.ID) assert.Equal(t, "Alice@Example.com", got.Email) assert.Equal(t, "Alice", got.Name) assert.Equal(t, []string{"admin", "user"}, got.Roles) assert.False(t, got.CreatedAt.IsZero()) assert.False(t, got.UpdatedAt.IsZero()) } func TestPGUser_Upsert_UpdatesAndPreservesCreatedAt(t *testing.T) { ctx := context.Background() s, _ := newPGUserStore(t) id := uuid.NewString() require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "old@example.com", Name: "Old"})) first, err := s.Get(ctx, id) require.NoError(t, err) origCreated := first.CreatedAt require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "new@example.com", Name: "New", Roles: []string{"r"}})) second, err := s.Get(ctx, id) require.NoError(t, err) assert.Equal(t, "new@example.com", second.Email, "email 應更新") assert.Equal(t, "New", second.Name, "name 應更新") assert.Equal(t, []string{"r"}, second.Roles, "roles 應更新") assert.Equal(t, origCreated, second.CreatedAt, "created_at 應保留(ON CONFLICT 不動 created_at)") assert.True(t, second.UpdatedAt.After(origCreated) || second.UpdatedAt.Equal(origCreated), "updated_at 應 >= created_at") } // TestPGUser_Upsert_EmailUniqueCaseInsensitive 驗證 uq_users_email_lower: // 兩個不同 sub 共用同 email(差大小寫)→ 第二筆撞 lower(email) unique(23505)。 // // 這是 D1-B 下的已知資料異常邊界(雛形原樣回錯、不靜默吞)。 func TestPGUser_Upsert_EmailUniqueCaseInsensitive(t *testing.T) { ctx := context.Background() s, _ := newPGUserStore(t) require.NoError(t, s.Upsert(ctx, &User{ID: uuid.NewString(), Email: "dup@example.com"})) err := s.Upsert(ctx, &User{ID: uuid.NewString(), Email: "DUP@example.com"}) require.Error(t, err, "不同 sub 共用同 email(差大小寫)應撞 uq_users_email_lower") var pgErr *pgconn.PgError if assert.ErrorAs(t, err, &pgErr) { assert.Equal(t, "23505", pgErr.Code, "應為 unique_violation") } } func TestPGUser_Get_NotFound(t *testing.T) { ctx := context.Background() s, _ := newPGUserStore(t) _, err := s.Get(ctx, uuid.NewString()) assert.ErrorIs(t, err, ErrNotFound) } // TestPGUser_Upsert_NilRolesEmptyArray 驗證 roles nil → 寫空陣列 round-trip 回空/ nil。 func TestPGUser_Upsert_NilRolesEmptyArray(t *testing.T) { ctx := context.Background() s, _ := newPGUserStore(t) id := uuid.NewString() require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "noroles@example.com"})) got, err := s.Get(ctx, id) require.NoError(t, err) assert.Empty(t, got.Roles, "roles 預設應為空(NOT NULL DEFAULT '{}')") } // TestPGUser_Upsert_NameNullable 驗證 name 空字串 → 寫 NULL → 掃回空字串。 func TestPGUser_Upsert_NameNullable(t *testing.T) { ctx := context.Background() s, tdb := newPGUserStore(t) id := uuid.NewString() require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "noname@example.com"})) // 直接查 DB 確認 name 欄位是 NULL(非空字串)。 var nameIsNull bool require.NoError(t, tdb.Pool.QueryRow(ctx, `SELECT name IS NULL FROM users WHERE id = $1`, id).Scan(&nameIsNull)) assert.True(t, nameIsNull, "空 name 應寫成 NULL") got, err := s.Get(ctx, id) require.NoError(t, err) assert.Equal(t, "", got.Name, "NULL name 應掃回空字串") }