diff --git a/backend/internal/store/user_test.go b/backend/internal/store/user_test.go new file mode 100644 index 0000000..d335933 --- /dev/null +++ b/backend/internal/store/user_test.go @@ -0,0 +1,185 @@ +package store + +import ( + "context" + "errors" + "testing" + "time" +) + +// newUserRepo 返回一个可测试的 UserRepository 实现。 +// 如需测试 Pg 实现,可在此替换为连接真实 DB 的版本。 +func newUserRepo() UserRepository { + return NewMemUserRepository() +} + +func TestUserRepository_Create(t *testing.T) { + repo := newUserRepo() + ctx := context.Background() + + id, err := repo.Create(ctx, "alice", "hash123") + if err != nil { + t.Fatalf("Create failed: %v", err) + } + if id == "" { + t.Fatal("expected non-empty ID") + } + + // 重复用户名应返回 ErrUsernameTaken + _, err = repo.Create(ctx, "alice", "hash456") + if !errors.Is(err, ErrUsernameTaken) { + t.Fatalf("expected ErrUsernameTaken, got %v", err) + } +} + +func TestUserRepository_FindByUsername(t *testing.T) { + repo := newUserRepo() + ctx := context.Background() + + _, err := repo.Create(ctx, "bob", "hash_bob") + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + user, err := repo.FindByUsername(ctx, "bob") + if err != nil { + t.Fatalf("FindByUsername failed: %v", err) + } + if user.Username != "bob" { + t.Fatalf("expected username bob, got %s", user.Username) + } + if user.PasswordHash != "hash_bob" { + t.Fatalf("expected password hash hash_bob, got %s", user.PasswordHash) + } + + // 不存在的用户 + _, err = repo.FindByUsername(ctx, "nobody") + if !errors.Is(err, ErrUserNotFound) { + t.Fatalf("expected ErrUserNotFound, got %v", err) + } +} + +func TestUserRepository_FindByID(t *testing.T) { + repo := newUserRepo() + ctx := context.Background() + + id, err := repo.Create(ctx, "charlie", "hash_charlie") + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + user, err := repo.FindByID(ctx, id) + if err != nil { + t.Fatalf("FindByID failed: %v", err) + } + if user.ID != id { + t.Fatalf("expected ID %s, got %s", id, user.ID) + } + if user.Username != "charlie" { + t.Fatalf("expected username charlie, got %s", user.Username) + } + + // 不存在的 ID + _, err = repo.FindByID(ctx, "nonexistent-uuid") + if !errors.Is(err, ErrUserNotFound) { + t.Fatalf("expected ErrUserNotFound, got %v", err) + } +} + +func TestUserRepository_RefreshToken(t *testing.T) { + repo := newUserRepo() + ctx := context.Background() + + userID, err := repo.Create(ctx, "dave", "hash_dave") + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + tokenHash := "abc123hash" + expiresAt := time.Now().Add(7 * 24 * time.Hour) + + // 保存 token + if err := repo.SaveRefreshToken(ctx, userID, tokenHash, expiresAt); err != nil { + t.Fatalf("SaveRefreshToken failed: %v", err) + } + + // 查找 token + foundUserID, err := repo.FindRefreshToken(ctx, tokenHash) + if err != nil { + t.Fatalf("FindRefreshToken failed: %v", err) + } + if foundUserID != userID { + t.Fatalf("expected userID %s, got %s", userID, foundUserID) + } + + // 不存在的 token + _, err = repo.FindRefreshToken(ctx, "nonexistent") + if !errors.Is(err, ErrRefreshTokenNotFound) { + t.Fatalf("expected ErrRefreshTokenNotFound, got %v", err) + } + + // 删除 token + if err := repo.DeleteRefreshToken(ctx, tokenHash); err != nil { + t.Fatalf("DeleteRefreshToken failed: %v", err) + } + _, err = repo.FindRefreshToken(ctx, tokenHash) + if !errors.Is(err, ErrRefreshTokenNotFound) { + t.Fatalf("expected ErrRefreshTokenNotFound after delete, got %v", err) + } +} + +func TestUserRepository_DeleteUserRefreshTokens(t *testing.T) { + repo := newUserRepo() + ctx := context.Background() + + userID, err := repo.Create(ctx, "eve", "hash_eve") + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + // 保存多个 token + for i := 0; i < 3; i++ { + tokenHash := "token_" + string(rune('a'+i)) + expiresAt := time.Now().Add(7 * 24 * time.Hour) + if err := repo.SaveRefreshToken(ctx, userID, tokenHash, expiresAt); err != nil { + t.Fatalf("SaveRefreshToken failed: %v", err) + } + } + + // 删除用户所有 token + if err := repo.DeleteUserRefreshTokens(ctx, userID); err != nil { + t.Fatalf("DeleteUserRefreshTokens failed: %v", err) + } + + // 验证全部删除 + for i := 0; i < 3; i++ { + tokenHash := "token_" + string(rune('a'+i)) + _, err := repo.FindRefreshToken(ctx, tokenHash) + if !errors.Is(err, ErrRefreshTokenNotFound) { + t.Fatalf("expected ErrRefreshTokenNotFound for token_%c, got %v", 'a'+i, err) + } + } +} + +func TestUserRepository_ExpiredRefreshToken(t *testing.T) { + repo := newUserRepo() + ctx := context.Background() + + userID, err := repo.Create(ctx, "frank", "hash_frank") + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + tokenHash := "expired_token" + expiresAt := time.Now().Add(-1 * time.Hour) // 已过期 + + if err := repo.SaveRefreshToken(ctx, userID, tokenHash, expiresAt); err != nil { + t.Fatalf("SaveRefreshToken failed: %v", err) + } + + // 过期 token 应返回 ErrRefreshTokenNotFound + _, err = repo.FindRefreshToken(ctx, tokenHash) + if !errors.Is(err, ErrRefreshTokenNotFound) { + t.Fatalf("expected ErrRefreshTokenNotFound for expired token, got %v", err) + } +}