Root causes of the remaining 5 CI PG failures (and the hidden flaky set):
1. All PG-integration packages shared ONE database under parallel package
execution - migrations/seeds/truncates raced (nondeterministic failure
sets, locally and in CI).
Fix: RequireTestDatabaseURL/SkipIfNoTestDatabaseURL now derive a
per-package database (<base>_<pkgHash>, advisory-lock-guarded
CREATE); 9 raw-env straggler tests funnelled through the helpers;
db-package tests call it unqualified (no import cycle).
2. Approval-chain fixtures were not production-shaped: freezeReviewSnapshot
only honors head_snapshot_id, fake content hashes never froze a
snapshot, so ApproverApprove rejected with 'approved requires frozen
review_snapshot_id'. Errors were swallowed (_, _ =) hiding this.
Fix (ownership-respecting, arch-enforced):
- docrevision.SeedContentSnapshotForTest seeds the ledger tables it owns
- docgov GovernanceEngine.FreezeHeadSnapshotForTest freezes head/review
pointers on document_records (docgov-owned) and clears engine cache
- all four integration fixtures use the pair; swallowed errors checked
3. Fixed-name fixtures collide once a package shares one DB (projects
uq_projects_name_global): uniquified in projectinstance + server tests.
4. CAS/ctxmgr tests never bootstrapped migrations (relied on other packages
having migrated the shared DB): explicit RunMigrationsOnceForTest.
Verified: full parallel short suite = 75 packages, 0 failures against an
isolated throwaway PG. CI skip list removed - short tests fully required.
104 lines
3.3 KiB
Go
104 lines
3.3 KiB
Go
package auth_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/auth"
|
|
"chat-agent-system/internal/config"
|
|
)
|
|
|
|
func TestGenerateTempPassword(t *testing.T) {
|
|
pw, err := auth.GenerateTempPassword(16)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(pw) != 16 {
|
|
t.Fatalf("len=%d", len(pw))
|
|
}
|
|
for _, r := range pw {
|
|
if !strings.ContainsRune("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789", r) {
|
|
t.Fatalf("unexpected rune %q", r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateUserRejectsEmptyPasswordHash(t *testing.T) {
|
|
store := auth.NewMemoryStore(&config.Config{Env: "dev", AllowInMemory: true})
|
|
_, err := store.CreateUser(t.Context(), &auth.UserAccount{
|
|
Username: "nohash",
|
|
Name: "无哈希",
|
|
Status: auth.StatusActive,
|
|
RoleID: "role-guest",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "password hash required") {
|
|
t.Fatalf("expected password hash required, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAdminResetPassword_Service(t *testing.T) {
|
|
cfg := &config.Config{Env: "dev", AllowInMemory: true, AdminUsername: "admin", AdminPassword: "admin123"}
|
|
store := auth.NewMemoryStore(cfg)
|
|
svc := auth.NewService(store, nil, cfg)
|
|
|
|
created, err := svc.CreateUserByAdmin(t.Context(), auth.CreateUserRequest{
|
|
Username: "svc_reset",
|
|
Name: "服务重置",
|
|
InitialPassword: "InitPass123",
|
|
RoleID: "role-engineer",
|
|
Position: "方案工程师",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := svc.AdminResetPassword(t.Context(), created.ID, 1, "req_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.TempPassword == "" || !result.MustChangePassword {
|
|
t.Fatalf("bad result %+v", result)
|
|
}
|
|
|
|
_, err = svc.Login(t.Context(), "svc_reset", "InitPass123")
|
|
if err == nil {
|
|
t.Fatal("old password should fail")
|
|
}
|
|
session, err := svc.Login(t.Context(), "svc_reset", result.TempPassword)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !session.MustChangePassword {
|
|
t.Fatal("expected must_change_password")
|
|
}
|
|
}
|
|
|
|
func TestLoginMissingPasswordHash(t *testing.T) {
|
|
cfg := &config.Config{Env: "dev", AllowInMemory: true, AdminUsername: "admin", AdminPassword: "admin123"}
|
|
store := auth.NewMemoryStore(cfg)
|
|
// Bypass CreateUser guard by mutating seeded user hash after create via UpdateUserPassword empty is not allowed;
|
|
// plant via direct store map is not exported — use CreateUser with hash then clear via UpdateUserPassword with invalid.
|
|
// Instead: create with hash then overwrite stored hash through UpdateUserPassword with a sentinel that CheckPassword rejects,
|
|
// and separately verify empty-hash path by creating through internal seed is hard.
|
|
// Use Login against unknown user for 401 uniformity, and empty-hash via GetUserByID mutation on MemoryStore UpdateUserPassword
|
|
// with hash "" is allowed by UpdateUserPassword — plant empty hash that way.
|
|
created, err := store.CreateUser(t.Context(), &auth.UserAccount{
|
|
Username: "emptyhash",
|
|
Name: "空哈希",
|
|
PasswordHash: "placeholder",
|
|
Status: auth.StatusActive,
|
|
RoleID: "role-guest",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.UpdateUserPassword(t.Context(), created.ID, "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := auth.NewService(store, nil, cfg)
|
|
_, err = svc.Login(t.Context(), "emptyhash", "anything")
|
|
if err == nil || err.Error() != "用户名或密码错误" {
|
|
t.Fatalf("expected unified credential error, got %v", err)
|
|
}
|
|
}
|