To gitea and beyond, let's go(-yco)
This commit is contained in:
41
internal/repositories/account_deletion_repository.go
Normal file
41
internal/repositories/account_deletion_repository.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
type AccountDeletionRepository interface {
|
||||
Create(req *database.AccountDeletionRequest) error
|
||||
GetByTokenHash(hash string) (*database.AccountDeletionRequest, error)
|
||||
DeleteByID(id uint) error
|
||||
DeleteByUserID(userID uint) error
|
||||
}
|
||||
|
||||
type accountDeletionRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAccountDeletionRepository(db *gorm.DB) AccountDeletionRepository {
|
||||
return &accountDeletionRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *accountDeletionRepository) Create(req *database.AccountDeletionRequest) error {
|
||||
return r.db.Create(req).Error
|
||||
}
|
||||
|
||||
func (r *accountDeletionRepository) GetByTokenHash(hash string) (*database.AccountDeletionRequest, error) {
|
||||
var request database.AccountDeletionRequest
|
||||
if err := r.db.Where("token_hash = ?", hash).First(&request).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &request, nil
|
||||
}
|
||||
|
||||
func (r *accountDeletionRepository) DeleteByID(id uint) error {
|
||||
return r.db.Delete(&database.AccountDeletionRequest{}, id).Error
|
||||
}
|
||||
|
||||
func (r *accountDeletionRepository) DeleteByUserID(userID uint) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&database.AccountDeletionRequest{}).Error
|
||||
}
|
||||
232
internal/repositories/account_deletion_repository_test.go
Normal file
232
internal/repositories/account_deletion_repository_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
func TestAccountDeletionRepository_Create(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
request := suite.CreateTestAccountDeletionRequest(user.ID, "token-hash-123")
|
||||
|
||||
if suite.GetAccountDeletionRequestCount() != 1 {
|
||||
t.Errorf("Expected 1 account deletion request, got %d", suite.GetAccountDeletionRequestCount())
|
||||
}
|
||||
|
||||
if request.ID == 0 {
|
||||
t.Error("Expected request ID to be assigned")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate token hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
suite.CreateTestAccountDeletionRequest(user.ID, "duplicate-token")
|
||||
|
||||
request2 := &database.AccountDeletionRequest{
|
||||
UserID: user.ID,
|
||||
TokenHash: "duplicate-token",
|
||||
}
|
||||
err := suite.DeletionRepo.Create(request2)
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate token hash")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("request with invalid user", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
request := &database.AccountDeletionRequest{
|
||||
UserID: 999,
|
||||
TokenHash: "token-hash-123",
|
||||
}
|
||||
err := suite.DeletionRepo.Create(request)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for invalid user (SQLite allows this): %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountDeletionRepository_GetByTokenHash(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing request", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
suite.CreateTestAccountDeletionRequest(user.ID, "token-hash-123")
|
||||
|
||||
retrieved, err := suite.DeletionRepo.GetByTokenHash("token-hash-123")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected request, got nil")
|
||||
}
|
||||
|
||||
if retrieved.TokenHash != "token-hash-123" {
|
||||
t.Errorf("Expected token hash 'token-hash-123', got %s", retrieved.TokenHash)
|
||||
}
|
||||
if retrieved.UserID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, retrieved.UserID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existing token hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
_, err := suite.DeletionRepo.GetByTokenHash("nonexistent-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existing token hash")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountDeletionRepository_DeleteByID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful delete", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
request := suite.CreateTestAccountDeletionRequest(user.ID, "token-hash-123")
|
||||
|
||||
err := suite.DeletionRepo.DeleteByID(request.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.DeletionRepo.GetByTokenHash("token-hash-123")
|
||||
if err == nil {
|
||||
t.Error("Expected error for deleted request")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete non-existing request", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
err := suite.DeletionRepo.DeleteByID(999)
|
||||
if err != nil {
|
||||
t.Fatalf("Delete should succeed even for non-existing request (GORM behavior)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountDeletionRepository_DeleteByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful delete", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
suite.CreateTestAccountDeletionRequest(user.ID, "token-hash-1")
|
||||
|
||||
err := suite.DeletionRepo.DeleteByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if suite.GetAccountDeletionRequestCount() != 0 {
|
||||
t.Errorf("Expected 0 requests after delete, got %d", suite.GetAccountDeletionRequestCount())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete for user without requests", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
err := suite.DeletionRepo.DeleteByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Delete should succeed even for user without requests (GORM behavior)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountDeletionRepository_EdgeCases(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty token hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
request := &database.AccountDeletionRequest{
|
||||
UserID: user.ID,
|
||||
TokenHash: "",
|
||||
}
|
||||
err := suite.DeletionRepo.Create(request)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for empty token hash: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero user ID", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
request := &database.AccountDeletionRequest{
|
||||
UserID: 0,
|
||||
TokenHash: "token-hash-123",
|
||||
}
|
||||
err := suite.DeletionRepo.Create(request)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for zero user ID: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("very long token hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
longToken := strings.Repeat("a", 300)
|
||||
request := &database.AccountDeletionRequest{
|
||||
UserID: user.ID,
|
||||
TokenHash: longToken,
|
||||
}
|
||||
err := suite.DeletionRepo.Create(request)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for long token hash: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountDeletionRepository_ConcurrentAccess(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("concurrent creates", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
|
||||
suite.CreateTestAccountDeletionRequest(user1.ID, "token-hash-1")
|
||||
suite.CreateTestAccountDeletionRequest(user2.ID, "token-hash-2")
|
||||
|
||||
if suite.GetAccountDeletionRequestCount() != 2 {
|
||||
t.Errorf("Expected 2 requests, got %d", suite.GetAccountDeletionRequestCount())
|
||||
}
|
||||
})
|
||||
}
|
||||
628
internal/repositories/database_test.go
Normal file
628
internal/repositories/database_test.go
Normal file
@@ -0,0 +1,628 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/database"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestDatabase_AssertUserExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing user", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
retrieved := suite.AssertUserExists(user.ID)
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected user, got nil")
|
||||
}
|
||||
|
||||
if retrieved.ID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, retrieved.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_AssertUserNotExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("non-existing user", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
suite.AssertUserNotExists(999)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_AssertPostExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing post", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
retrieved := suite.AssertPostExists(post.ID)
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected post, got nil")
|
||||
}
|
||||
|
||||
if retrieved.ID != post.ID {
|
||||
t.Errorf("Expected post ID %d, got %d", post.ID, retrieved.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_AssertPostNotExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("non-existing post", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
suite.AssertPostNotExists(999)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_AssertVoteExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
vote := suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
retrieved := suite.AssertVoteExists(vote.ID)
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected vote, got nil")
|
||||
}
|
||||
|
||||
if retrieved.ID != vote.ID {
|
||||
t.Errorf("Expected vote ID %d, got %d", vote.ID, retrieved.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_AssertVoteNotExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("non-existing vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
suite.AssertVoteNotExists(999)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_AssertAccountDeletionRequestExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing request", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
request := suite.CreateTestAccountDeletionRequest(user.ID, "token-hash-123")
|
||||
|
||||
retrieved := suite.AssertAccountDeletionRequestExists("token-hash-123")
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected request, got nil")
|
||||
}
|
||||
|
||||
if retrieved.ID != request.ID {
|
||||
t.Errorf("Expected request ID %d, got %d", request.ID, retrieved.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_AssertAccountDeletionRequestNotExists(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("non-existing request", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
suite.AssertAccountDeletionRequestNotExists("nonexistent-token")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_GetUserCount(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
count := suite.GetUserCount()
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 users, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with users", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
|
||||
count := suite.GetUserCount()
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 users, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_GetPostCount(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
count := suite.GetPostCount()
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 posts, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
suite.CreateTestPost(user.ID, "Post 1", "https://example.com/1", "Content 1")
|
||||
suite.CreateTestPost(user.ID, "Post 2", "https://example.com/2", "Content 2")
|
||||
|
||||
count := suite.GetPostCount()
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 posts, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_GetVoteCount(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
count := suite.GetVoteCount()
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 votes, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
post := suite.CreateTestPost(user1.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
suite.CreateTestVote(user1.ID, post.ID, database.VoteUp)
|
||||
suite.CreateTestVote(user2.ID, post.ID, database.VoteDown)
|
||||
|
||||
count := suite.GetVoteCount()
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 votes, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_GetAccountDeletionRequestCount(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
count := suite.GetAccountDeletionRequestCount()
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 requests, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with requests", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
suite.CreateTestAccountDeletionRequest(user1.ID, "token-hash-1")
|
||||
suite.CreateTestAccountDeletionRequest(user2.ID, "token-hash-2")
|
||||
|
||||
count := suite.GetAccountDeletionRequestCount()
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 requests, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_Reset(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("reset clears all data", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
suite.CreateTestAccountDeletionRequest(user.ID, "token-hash-123")
|
||||
|
||||
if suite.GetUserCount() != 1 {
|
||||
t.Errorf("Expected 1 user before reset, got %d", suite.GetUserCount())
|
||||
}
|
||||
if suite.GetPostCount() != 1 {
|
||||
t.Errorf("Expected 1 post before reset, got %d", suite.GetPostCount())
|
||||
}
|
||||
if suite.GetVoteCount() != 1 {
|
||||
t.Errorf("Expected 1 vote before reset, got %d", suite.GetVoteCount())
|
||||
}
|
||||
if suite.GetAccountDeletionRequestCount() != 1 {
|
||||
t.Errorf("Expected 1 request before reset, got %d", suite.GetAccountDeletionRequestCount())
|
||||
}
|
||||
|
||||
suite.Reset()
|
||||
|
||||
if suite.GetUserCount() != 0 {
|
||||
t.Errorf("Expected 0 users after reset, got %d", suite.GetUserCount())
|
||||
}
|
||||
if suite.GetPostCount() != 0 {
|
||||
t.Errorf("Expected 0 posts after reset, got %d", suite.GetPostCount())
|
||||
}
|
||||
if suite.GetVoteCount() != 0 {
|
||||
t.Errorf("Expected 0 votes after reset, got %d", suite.GetVoteCount())
|
||||
}
|
||||
if suite.GetAccountDeletionRequestCount() != 0 {
|
||||
t.Errorf("Expected 0 requests after reset, got %d", suite.GetAccountDeletionRequestCount())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_CreateTestUser(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
if user.ID == 0 {
|
||||
t.Error("Expected user ID to be assigned")
|
||||
}
|
||||
|
||||
if user.Username != "testuser" {
|
||||
t.Errorf("Expected username 'testuser', got %s", user.Username)
|
||||
}
|
||||
|
||||
if user.Email != "test@example.com" {
|
||||
t.Errorf("Expected email 'test@example.com', got %s", user.Email)
|
||||
}
|
||||
|
||||
if !user.EmailVerified {
|
||||
t.Error("Expected user to be email verified")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_CreateTestPost(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
if post.ID == 0 {
|
||||
t.Error("Expected post ID to be assigned")
|
||||
}
|
||||
|
||||
if post.Title != "Test Post" {
|
||||
t.Errorf("Expected title 'Test Post', got %s", post.Title)
|
||||
}
|
||||
|
||||
if post.URL != "https://example.com" {
|
||||
t.Errorf("Expected URL 'https://example.com', got %s", post.URL)
|
||||
}
|
||||
|
||||
if post.Content != "Test content" {
|
||||
t.Errorf("Expected content 'Test content', got %s", post.Content)
|
||||
}
|
||||
|
||||
if post.AuthorID == nil || *post.AuthorID != user.ID {
|
||||
t.Errorf("Expected author ID %d, got %v", user.ID, post.AuthorID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_CreateTestVote(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
vote := suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
if vote.ID == 0 {
|
||||
t.Error("Expected vote ID to be assigned")
|
||||
}
|
||||
|
||||
if vote.UserID == nil || *vote.UserID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %v", user.ID, vote.UserID)
|
||||
}
|
||||
|
||||
if vote.PostID != post.ID {
|
||||
t.Errorf("Expected post ID %d, got %d", post.ID, vote.PostID)
|
||||
}
|
||||
|
||||
if vote.Type != database.VoteUp {
|
||||
t.Errorf("Expected vote type %v, got %v", database.VoteUp, vote.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabase_CreateTestAccountDeletionRequest(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
request := suite.CreateTestAccountDeletionRequest(user.ID, "token-hash-123")
|
||||
|
||||
if request.ID == 0 {
|
||||
t.Error("Expected request ID to be assigned")
|
||||
}
|
||||
|
||||
if request.UserID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, request.UserID)
|
||||
}
|
||||
|
||||
if request.TokenHash != "token-hash-123" {
|
||||
t.Errorf("Expected token hash 'token-hash-123', got %s", request.TokenHash)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyPagination(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
limit int
|
||||
offset int
|
||||
setupQuery func(*gorm.DB) *gorm.DB
|
||||
verifyPagination func(*testing.T, *gorm.DB, int, int)
|
||||
}{
|
||||
{
|
||||
name: "limit > 0 and offset > 0",
|
||||
limit: 10,
|
||||
offset: 5,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 20; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
if len(users) != limit {
|
||||
t.Errorf("Expected %d users, got %d", limit, len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit > 0 and offset = 0",
|
||||
limit: 5,
|
||||
offset: 0,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 10; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
if len(users) != limit {
|
||||
t.Errorf("Expected %d users, got %d", limit, len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit = 0 (should not apply limit)",
|
||||
limit: 0,
|
||||
offset: 5,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 10; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
expected := 5
|
||||
if len(users) != expected {
|
||||
t.Errorf("Expected %d users with offset %d, got %d", expected, offset, len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset = 0 (should not apply offset)",
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 15; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
if len(users) != limit {
|
||||
t.Errorf("Expected %d users, got %d", limit, len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit = 0 and offset = 0 (should not apply pagination)",
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 10; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
if len(users) != 10 {
|
||||
t.Errorf("Expected all 10 users, got %d", len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative limit (should not apply limit)",
|
||||
limit: -5,
|
||||
offset: 10,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 20; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
expected := 10
|
||||
if len(users) != expected {
|
||||
t.Errorf("Expected %d users with offset %d, got %d", expected, offset, len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative offset (should not apply offset)",
|
||||
limit: 10,
|
||||
offset: -5,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 15; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
if len(users) != limit {
|
||||
t.Errorf("Expected %d users, got %d", limit, len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "large limit and offset values",
|
||||
limit: 1000,
|
||||
offset: 500,
|
||||
setupQuery: func(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&database.User{})
|
||||
},
|
||||
verifyPagination: func(t *testing.T, query *gorm.DB, limit, offset int) {
|
||||
for i := 0; i < 2000; i++ {
|
||||
suite.CreateTestUser(
|
||||
fmt.Sprintf("testuser_%d", i),
|
||||
fmt.Sprintf("user%d@example.com", i),
|
||||
"password123",
|
||||
)
|
||||
}
|
||||
|
||||
var users []database.User
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Query failed: %v", result.Error)
|
||||
}
|
||||
|
||||
if len(users) != limit {
|
||||
t.Errorf("Expected %d users, got %d", limit, len(users))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
baseQuery := tt.setupQuery(suite.DB)
|
||||
paginatedQuery := ApplyPagination(baseQuery, tt.limit, tt.offset)
|
||||
|
||||
tt.verifyPagination(t, paginatedQuery, tt.limit, tt.offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
237
internal/repositories/fixtures.go
Normal file
237
internal/repositories/fixtures.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
type TestDatabase struct {
|
||||
DB *gorm.DB
|
||||
UserRepo UserRepository
|
||||
PostRepo PostRepository
|
||||
VoteRepo VoteRepository
|
||||
DeletionRepo AccountDeletionRepository
|
||||
RefreshTokenRepo *RefreshTokenRepository
|
||||
}
|
||||
|
||||
func NewTestDatabase(t *testing.T) *TestDatabase {
|
||||
dbName := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect to test database: %v", err)
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(
|
||||
&database.User{},
|
||||
&database.Post{},
|
||||
&database.Vote{},
|
||||
&database.AccountDeletionRequest{},
|
||||
&database.RefreshToken{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to migrate test database: %v", err)
|
||||
}
|
||||
|
||||
userRepo := NewUserRepository(db)
|
||||
postRepo := NewPostRepository(db)
|
||||
voteRepo := NewVoteRepository(db)
|
||||
deletionRepo := NewAccountDeletionRepository(db)
|
||||
refreshTokenRepo := NewRefreshTokenRepository(db)
|
||||
|
||||
return &TestDatabase{
|
||||
DB: db,
|
||||
UserRepo: userRepo,
|
||||
PostRepo: postRepo,
|
||||
VoteRepo: voteRepo,
|
||||
DeletionRepo: deletionRepo,
|
||||
RefreshTokenRepo: refreshTokenRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (td *TestDatabase) Cleanup() {
|
||||
if td.DB != nil {
|
||||
sqlDB, err := td.DB.DB()
|
||||
if err == nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (td *TestDatabase) Reset() {
|
||||
td.DB.Exec("DELETE FROM votes")
|
||||
td.DB.Exec("DELETE FROM posts")
|
||||
td.DB.Exec("DELETE FROM account_deletion_requests")
|
||||
td.DB.Exec("DELETE FROM refresh_tokens")
|
||||
td.DB.Exec("DELETE FROM users")
|
||||
}
|
||||
|
||||
func (td *TestDatabase) CreateTestUser(username, email, password string) *database.User {
|
||||
user := &database.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := td.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test user: %v", err))
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (td *TestDatabase) CreateTestPost(authorID uint, title, url, content string) *database.Post {
|
||||
post := &database.Post{
|
||||
Title: title,
|
||||
URL: url,
|
||||
Content: content,
|
||||
AuthorID: &authorID,
|
||||
}
|
||||
err := td.PostRepo.Create(post)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test post: %v", err))
|
||||
}
|
||||
return post
|
||||
}
|
||||
|
||||
func (td *TestDatabase) CreateTestVote(userID, postID uint, voteType database.VoteType) *database.Vote {
|
||||
vote := &database.Vote{
|
||||
UserID: &userID,
|
||||
PostID: postID,
|
||||
Type: voteType,
|
||||
}
|
||||
err := td.VoteRepo.Create(vote)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test vote: %v", err))
|
||||
}
|
||||
return vote
|
||||
}
|
||||
|
||||
func (td *TestDatabase) CreateTestAccountDeletionRequest(userID uint, tokenHash string) *database.AccountDeletionRequest {
|
||||
request := &database.AccountDeletionRequest{
|
||||
UserID: userID,
|
||||
TokenHash: tokenHash,
|
||||
}
|
||||
err := td.DeletionRepo.Create(request)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test account deletion request: %v", err))
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
type TestSuite struct {
|
||||
*TestDatabase
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func NewTestSuite(t *testing.T) *TestSuite {
|
||||
testDB := NewTestDatabase(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
testDB.Cleanup()
|
||||
})
|
||||
|
||||
return &TestSuite{
|
||||
TestDatabase: testDB,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestSuite) Reset() {
|
||||
ts.TestDatabase.Reset()
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertUserExists(userID uint) *database.User {
|
||||
user, err := ts.UserRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
ts.t.Fatalf("Expected user %d to exist, got error: %v", userID, err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertUserNotExists(userID uint) {
|
||||
_, err := ts.UserRepo.GetByID(userID)
|
||||
if err == nil {
|
||||
ts.t.Fatalf("Expected user %d to not exist", userID)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertPostExists(postID uint) *database.Post {
|
||||
post, err := ts.PostRepo.GetByID(postID)
|
||||
if err != nil {
|
||||
ts.t.Fatalf("Expected post %d to exist, got error: %v", postID, err)
|
||||
}
|
||||
return post
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertPostNotExists(postID uint) {
|
||||
_, err := ts.PostRepo.GetByID(postID)
|
||||
if err == nil {
|
||||
ts.t.Fatalf("Expected post %d to not exist", postID)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertVoteExists(voteID uint) *database.Vote {
|
||||
vote, err := ts.VoteRepo.GetByID(voteID)
|
||||
if err != nil {
|
||||
ts.t.Fatalf("Expected vote %d to exist, got error: %v", voteID, err)
|
||||
}
|
||||
return vote
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertVoteNotExists(voteID uint) {
|
||||
_, err := ts.VoteRepo.GetByID(voteID)
|
||||
if err == nil {
|
||||
ts.t.Fatalf("Expected vote %d to not exist", voteID)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertAccountDeletionRequestExists(tokenHash string) *database.AccountDeletionRequest {
|
||||
request, err := ts.DeletionRepo.GetByTokenHash(tokenHash)
|
||||
if err != nil {
|
||||
ts.t.Fatalf("Expected account deletion request with token %s to exist, got error: %v", tokenHash, err)
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func (ts *TestSuite) AssertAccountDeletionRequestNotExists(tokenHash string) {
|
||||
_, err := ts.DeletionRepo.GetByTokenHash(tokenHash)
|
||||
if err == nil {
|
||||
ts.t.Fatalf("Expected account deletion request with token %s to not exist", tokenHash)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestSuite) GetUserCount() int64 {
|
||||
var count int64
|
||||
ts.DB.Model(&database.User{}).Count(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
func (ts *TestSuite) GetPostCount() int64 {
|
||||
var count int64
|
||||
ts.DB.Model(&database.Post{}).Count(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
func (ts *TestSuite) GetVoteCount() int64 {
|
||||
var count int64
|
||||
ts.DB.Model(&database.Vote{}).Count(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
func (ts *TestSuite) GetAccountDeletionRequestCount() int64 {
|
||||
var count int64
|
||||
ts.DB.Model(&database.AccountDeletionRequest{}).Count(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
func (ts *TestSuite) CreateInvalidUserID() *uint {
|
||||
id := uint(999)
|
||||
return &id
|
||||
}
|
||||
13
internal/repositories/pagination.go
Normal file
13
internal/repositories/pagination.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package repositories
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func ApplyPagination(query *gorm.DB, limit, offset int) *gorm.DB {
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
return query
|
||||
}
|
||||
158
internal/repositories/post_repository.go
Normal file
158
internal/repositories/post_repository.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
type PostRepository interface {
|
||||
Create(post *database.Post) error
|
||||
GetByID(id uint) (*database.Post, error)
|
||||
GetAll(limit, offset int) ([]database.Post, error)
|
||||
GetByUserID(userID uint, limit, offset int) ([]database.Post, error)
|
||||
Update(post *database.Post) error
|
||||
Delete(id uint) error
|
||||
Count() (int64, error)
|
||||
CountByUserID(userID uint) (int64, error)
|
||||
GetTopPosts(limit int) ([]database.Post, error)
|
||||
GetNewestPosts(limit int) ([]database.Post, error)
|
||||
Search(query string, limit, offset int) ([]database.Post, error)
|
||||
GetPostsByDeletedUsers() ([]database.Post, error)
|
||||
HardDeletePostsByDeletedUsers() (int64, error)
|
||||
HardDeleteAll() (int64, error)
|
||||
WithTx(tx *gorm.DB) PostRepository
|
||||
}
|
||||
|
||||
type postRepository struct {
|
||||
db *gorm.DB
|
||||
sanitizer *SearchSanitizer
|
||||
}
|
||||
|
||||
func NewPostRepository(db *gorm.DB) PostRepository {
|
||||
return &postRepository{
|
||||
db: db,
|
||||
sanitizer: NewSearchSanitizer(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *postRepository) Create(post *database.Post) error {
|
||||
return r.db.Create(post).Error
|
||||
}
|
||||
|
||||
func (r *postRepository) GetByID(id uint) (*database.Post, error) {
|
||||
var post database.Post
|
||||
err := r.db.Preload("Author").First(&post, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) GetAll(limit, offset int) ([]database.Post, error) {
|
||||
var posts []database.Post
|
||||
query := r.db.Preload("Author").Order("score DESC, created_at DESC, id DESC")
|
||||
query = ApplyPagination(query, limit, offset)
|
||||
|
||||
err := query.Find(&posts).Error
|
||||
return posts, err
|
||||
}
|
||||
|
||||
func (r *postRepository) GetByUserID(userID uint, limit, offset int) ([]database.Post, error) {
|
||||
var posts []database.Post
|
||||
query := r.db.Where("author_id = ?", userID).Preload("Author").Order("created_at DESC")
|
||||
query = ApplyPagination(query, limit, offset)
|
||||
|
||||
err := query.Find(&posts).Error
|
||||
return posts, err
|
||||
}
|
||||
|
||||
func (r *postRepository) Update(post *database.Post) error {
|
||||
return r.db.Save(post).Error
|
||||
}
|
||||
|
||||
func (r *postRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&database.Post{}, id).Error
|
||||
}
|
||||
|
||||
func (r *postRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&database.Post{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *postRepository) CountByUserID(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&database.Post{}).Where("author_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *postRepository) GetTopPosts(limit int) ([]database.Post, error) {
|
||||
var posts []database.Post
|
||||
query := r.db.Preload("Author").Order("score DESC, created_at DESC, id DESC")
|
||||
query = ApplyPagination(query, limit, 0)
|
||||
|
||||
err := query.Find(&posts).Error
|
||||
return posts, err
|
||||
}
|
||||
|
||||
func (r *postRepository) GetNewestPosts(limit int) ([]database.Post, error) {
|
||||
var posts []database.Post
|
||||
query := r.db.Preload("Author").Order("created_at DESC")
|
||||
query = ApplyPagination(query, limit, 0)
|
||||
|
||||
err := query.Find(&posts).Error
|
||||
return posts, err
|
||||
}
|
||||
|
||||
func (r *postRepository) Search(query string, limit, offset int) ([]database.Post, error) {
|
||||
var posts []database.Post
|
||||
|
||||
sanitizedQuery, err := r.sanitizer.SanitizeSearchQuery(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.sanitizer.ValidateSearchQuery(sanitizedQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sanitizedQuery == "" {
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
dbQuery := r.db.Preload("Author").Where("UPPER(title) LIKE UPPER(?) OR UPPER(content) LIKE UPPER(?)", "%"+sanitizedQuery+"%", "%"+sanitizedQuery+"%").Order("score DESC, created_at DESC, id DESC")
|
||||
dbQuery = ApplyPagination(dbQuery, limit, offset)
|
||||
|
||||
err = dbQuery.Find(&posts).Error
|
||||
return posts, err
|
||||
}
|
||||
|
||||
func (r *postRepository) WithTx(tx *gorm.DB) PostRepository {
|
||||
return &postRepository{db: tx, sanitizer: r.sanitizer}
|
||||
}
|
||||
|
||||
func (r *postRepository) GetPostsByDeletedUsers() ([]database.Post, error) {
|
||||
var posts []database.Post
|
||||
err := r.db.Unscoped().
|
||||
Preload("Author").
|
||||
Where("author_id IS NULL OR author_id IN (SELECT id FROM users WHERE deleted_at IS NOT NULL)").
|
||||
Find(&posts).Error
|
||||
return posts, err
|
||||
}
|
||||
|
||||
func (r *postRepository) HardDeletePostsByDeletedUsers() (int64, error) {
|
||||
result := r.db.Unscoped().
|
||||
Where("author_id IS NULL OR author_id IN (SELECT id FROM users WHERE deleted_at IS NOT NULL)").
|
||||
Delete(&database.Post{})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
func (r *postRepository) HardDeleteAll() (int64, error) {
|
||||
var rowsAffected int64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Unscoped().Where("1 = 1").Delete(&database.Post{})
|
||||
rowsAffected = result.RowsAffected
|
||||
return result.Error
|
||||
})
|
||||
return rowsAffected, err
|
||||
}
|
||||
804
internal/repositories/post_repository_test.go
Normal file
804
internal/repositories/post_repository_test.go
Normal file
@@ -0,0 +1,804 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
func TestPostRepository_Create(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
if suite.GetPostCount() != 1 {
|
||||
t.Errorf("Expected 1 post, got %d", suite.GetPostCount())
|
||||
}
|
||||
|
||||
if post.ID == 0 {
|
||||
t.Error("Expected post ID to be assigned")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("post with invalid author", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Test Post",
|
||||
URL: "https://example.com",
|
||||
Content: "Test content",
|
||||
AuthorID: suite.CreateInvalidUserID(),
|
||||
}
|
||||
err := suite.PostRepo.Create(post)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for invalid author (SQLite allows this): %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate URL", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
suite.CreateTestPost(user.ID, "Post 1", "https://example.com", "Content 1")
|
||||
|
||||
post2 := &database.Post{
|
||||
Title: "Post 2",
|
||||
URL: "https://example.com",
|
||||
Content: "Content 2",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err := suite.PostRepo.Create(post2)
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate URL")
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrDuplicatedKey) && !strings.Contains(err.Error(), "duplicate") && !strings.Contains(err.Error(), "UNIQUE constraint") {
|
||||
t.Errorf("Expected duplicate key error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_GetByID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing post", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
retrieved, err := suite.PostRepo.GetByID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected post, got nil")
|
||||
}
|
||||
|
||||
if retrieved.ID != post.ID {
|
||||
t.Errorf("Expected ID %d, got %d", post.ID, retrieved.ID)
|
||||
}
|
||||
if retrieved.Title != post.Title {
|
||||
t.Errorf("Expected title %s, got %s", post.Title, retrieved.Title)
|
||||
}
|
||||
if retrieved.URL != post.URL {
|
||||
t.Errorf("Expected URL %s, got %s", post.URL, retrieved.URL)
|
||||
}
|
||||
if retrieved.AuthorID == nil || post.AuthorID == nil {
|
||||
t.Error("Expected both AuthorID pointers to be non-nil")
|
||||
} else if *retrieved.AuthorID != *post.AuthorID {
|
||||
t.Errorf("Expected author ID %d, got %d", *post.AuthorID, *retrieved.AuthorID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existing post", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
_, err := suite.PostRepo.GetByID(999)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existing post")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_GetAll(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
posts, err := suite.PostRepo.GetAll(10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(posts) != 0 {
|
||||
t.Errorf("Expected 0 posts, got %d", len(posts))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
suite.CreateTestPost(user.ID, "Post 1", "https://example.com/1", "Content 1")
|
||||
suite.CreateTestPost(user.ID, "Post 2", "https://example.com/2", "Content 2")
|
||||
suite.CreateTestPost(user.ID, "Post 3", "https://example.com/3", "Content 3")
|
||||
|
||||
retrieved, err := suite.PostRepo.GetAll(10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 3 {
|
||||
t.Errorf("Expected 3 posts, got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with limit", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser2", "test2@example.com", "password123")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
suite.CreateTestPost(user.ID,
|
||||
"Post "+strconv.Itoa(i),
|
||||
"https://example.com/"+strconv.Itoa(i),
|
||||
"Content "+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
retrieved, err := suite.PostRepo.GetAll(2, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 posts, got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with offset", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser3", "test3@example.com", "password123")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
suite.CreateTestPost(user.ID,
|
||||
"Post "+strconv.Itoa(i),
|
||||
"https://example.com/"+strconv.Itoa(i),
|
||||
"Content "+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
retrieved, err := suite.PostRepo.GetAll(2, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 posts, got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_GetByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("user with posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
|
||||
suite.CreateTestPost(user1.ID, "User1 Post", "https://example.com/1", "Content 1")
|
||||
suite.CreateTestPost(user2.ID, "User2 Post", "https://example.com/2", "Content 2")
|
||||
suite.CreateTestPost(user1.ID, "User1 Post 2", "https://example.com/3", "Content 3")
|
||||
|
||||
retrieved, err := suite.PostRepo.GetByUserID(user1.ID, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 posts for user1, got %d", len(retrieved))
|
||||
}
|
||||
|
||||
for _, post := range retrieved {
|
||||
if post.AuthorID == nil || *post.AuthorID != user1.ID {
|
||||
t.Errorf("Expected author ID %d, got %v", user1.ID, post.AuthorID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user without posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("noposts", "noposts@example.com", "password123")
|
||||
|
||||
retrieved, err := suite.PostRepo.GetByUserID(user.ID, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 0 {
|
||||
t.Errorf("Expected 0 posts, got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_Update(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful update", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
post.Title = "Updated Post"
|
||||
post.Content = "Updated content"
|
||||
post.Score = 10
|
||||
post.UpVotes = 12
|
||||
post.DownVotes = 2
|
||||
err := suite.PostRepo.Update(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.PostRepo.GetByID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve post: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Title != "Updated Post" {
|
||||
t.Errorf("Expected title 'Updated Post', got %s", retrieved.Title)
|
||||
}
|
||||
if retrieved.Content != "Updated content" {
|
||||
t.Errorf("Expected content 'Updated content', got %s", retrieved.Content)
|
||||
}
|
||||
if retrieved.Score != 10 {
|
||||
t.Errorf("Expected score 10, got %d", retrieved.Score)
|
||||
}
|
||||
if retrieved.UpVotes != 12 {
|
||||
t.Errorf("Expected up votes 12, got %d", retrieved.UpVotes)
|
||||
}
|
||||
if retrieved.DownVotes != 2 {
|
||||
t.Errorf("Expected down votes 2, got %d", retrieved.DownVotes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_Delete(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful delete", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
err := suite.PostRepo.Delete(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.PostRepo.GetByID(post.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected error for deleted post")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_Count(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
count, err := suite.PostRepo.Count()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected count 0, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
suite.CreateTestPost(user.ID,
|
||||
"Post "+strconv.Itoa(i),
|
||||
"https://example.com/"+strconv.Itoa(i),
|
||||
"Content "+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
count, err := suite.PostRepo.Count()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 5 {
|
||||
t.Errorf("Expected count 5, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_GetTopPosts(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("with posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
post1 := &database.Post{
|
||||
Title: "Low Score",
|
||||
URL: "https://example.com/1",
|
||||
Content: "Content 1",
|
||||
AuthorID: &user.ID,
|
||||
Score: 1,
|
||||
}
|
||||
post2 := &database.Post{
|
||||
Title: "High Score",
|
||||
URL: "https://example.com/2",
|
||||
Content: "Content 2",
|
||||
AuthorID: &user.ID,
|
||||
Score: 10,
|
||||
}
|
||||
post3 := &database.Post{
|
||||
Title: "Medium Score",
|
||||
URL: "https://example.com/3",
|
||||
Content: "Content 3",
|
||||
AuthorID: &user.ID,
|
||||
Score: 5,
|
||||
}
|
||||
|
||||
suite.PostRepo.Create(post1)
|
||||
suite.PostRepo.Create(post2)
|
||||
suite.PostRepo.Create(post3)
|
||||
|
||||
retrieved, err := suite.PostRepo.GetTopPosts(2)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 top posts, got %d", len(retrieved))
|
||||
}
|
||||
|
||||
for i := 0; i < len(retrieved)-1; i++ {
|
||||
if retrieved[i].Score < retrieved[i+1].Score {
|
||||
t.Errorf("Posts not in descending order: %d < %d", retrieved[i].Score, retrieved[i+1].Score)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_GetNewestPosts(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("with posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
post1 := &database.Post{
|
||||
Title: "Old Post",
|
||||
URL: "https://example.com/1",
|
||||
Content: "Content 1",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
post2 := &database.Post{
|
||||
Title: "New Post",
|
||||
URL: "https://example.com/2",
|
||||
Content: "Content 2",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
|
||||
post1.CreatedAt = time.Now().Add(-2 * time.Hour)
|
||||
post2.CreatedAt = time.Now().Add(-1 * time.Hour)
|
||||
|
||||
suite.PostRepo.Create(post1)
|
||||
suite.PostRepo.Create(post2)
|
||||
|
||||
retrieved, err := suite.PostRepo.GetNewestPosts(2)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 newest posts, got %d", len(retrieved))
|
||||
}
|
||||
|
||||
if retrieved[0].CreatedAt.Before(retrieved[1].CreatedAt) {
|
||||
t.Error("Expected posts to be ordered by creation time (newest first)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_Search(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("with matching posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
suite.CreateTestPost(user.ID, "Golang Tutorial", "https://example.com/1", "Learn Go programming")
|
||||
suite.CreateTestPost(user.ID, "Python Guide", "https://example.com/2", "Learn Python programming")
|
||||
suite.CreateTestPost(user.ID, "Go Best Practices", "https://example.com/3", "Advanced Go techniques")
|
||||
|
||||
retrieved, err := suite.PostRepo.Search("Go", 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 posts matching 'Go', got %d", len(retrieved))
|
||||
}
|
||||
|
||||
for _, post := range retrieved {
|
||||
if post.Title != "Golang Tutorial" && post.Title != "Go Best Practices" {
|
||||
t.Errorf("Unexpected post in search results: %s", post.Title)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("case insensitive search", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
suite.CreateTestPost(user.ID, "Golang Tutorial", "https://example.com/1", "Learn Go programming")
|
||||
|
||||
retrieved, err := suite.PostRepo.Search("golang", 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 1 {
|
||||
t.Errorf("Expected 1 post matching 'golang', got %d", len(retrieved))
|
||||
}
|
||||
|
||||
if retrieved[0].Title != "Golang Tutorial" {
|
||||
t.Errorf("Expected 'Golang Tutorial', got %s", retrieved[0].Title)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no matching posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
retrieved, err := suite.PostRepo.Search("nonexistent", 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 0 {
|
||||
t.Errorf("Expected 0 posts matching 'nonexistent', got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("search with limit and offset", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser2", "test2@example.com", "password123")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
suite.CreateTestPost(user.ID,
|
||||
"Go Post "+strconv.Itoa(i),
|
||||
"https://example.com/go"+strconv.Itoa(i),
|
||||
"Go content "+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
retrieved, err := suite.PostRepo.Search("Go", 2, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 posts with limit, got %d", len(retrieved))
|
||||
}
|
||||
|
||||
retrieved, err = suite.PostRepo.Search("Go", 2, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 posts with offset, got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_CountByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("user with posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
suite.CreateTestPost(user.ID, "Post 1", "https://example.com/1", "Content 1")
|
||||
suite.CreateTestPost(user.ID, "Post 2", "https://example.com/2", "Content 2")
|
||||
suite.CreateTestPost(user.ID, "Post 3", "https://example.com/3", "Content 3")
|
||||
|
||||
count, err := suite.PostRepo.CountByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 3 {
|
||||
t.Errorf("Expected 3 posts, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user without posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
count, err := suite.PostRepo.CountByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 posts, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_WithTx(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("transaction repository", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
tx := suite.DB.Begin()
|
||||
defer tx.Rollback()
|
||||
|
||||
txPostRepo := suite.PostRepo.WithTx(tx)
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Transaction Post",
|
||||
URL: "https://example.com",
|
||||
Content: "Test content",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
|
||||
err := txPostRepo.Create(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := txPostRepo.GetByID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to find post in transaction, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Title != "Transaction Post" {
|
||||
t.Errorf("Expected title 'Transaction Post', got %s", retrieved.Title)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_GetPostsByDeletedUsers(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("posts by deleted users", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
err := suite.UserRepo.Delete(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete user: %v", err)
|
||||
}
|
||||
|
||||
posts, err := suite.PostRepo.GetPostsByDeletedUsers()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(posts) != 1 {
|
||||
t.Errorf("Expected 1 post by deleted user, got %d", len(posts))
|
||||
}
|
||||
|
||||
if posts[0].ID != post.ID {
|
||||
t.Errorf("Expected post ID %d, got %d", post.ID, posts[0].ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no posts by deleted users", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
posts, err := suite.PostRepo.GetPostsByDeletedUsers()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(posts) != 0 {
|
||||
t.Errorf("Expected 0 posts by deleted users, got %d", len(posts))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_HardDeletePostsByDeletedUsers(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("hard delete posts by deleted users", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
err := suite.UserRepo.Delete(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete user: %v", err)
|
||||
}
|
||||
|
||||
deletedCount, err := suite.PostRepo.HardDeletePostsByDeletedUsers()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if deletedCount != 1 {
|
||||
t.Errorf("Expected 1 post deleted, got %d", deletedCount)
|
||||
}
|
||||
|
||||
_, err = suite.PostRepo.GetByID(post.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected error for hard deleted post")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no posts to delete", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
deletedCount, err := suite.PostRepo.HardDeletePostsByDeletedUsers()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if deletedCount != 0 {
|
||||
t.Errorf("Expected 0 posts deleted, got %d", deletedCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_HardDeleteAll(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("hard delete all posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
suite.CreateTestPost(user.ID, "Post 1", "https://example.com/1", "Content 1")
|
||||
suite.CreateTestPost(user.ID, "Post 2", "https://example.com/2", "Content 2")
|
||||
|
||||
deletedCount, err := suite.PostRepo.HardDeleteAll()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if deletedCount != 2 {
|
||||
t.Errorf("Expected 2 posts deleted, got %d", deletedCount)
|
||||
}
|
||||
|
||||
posts, err := suite.PostRepo.GetAll(10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(posts) != 0 {
|
||||
t.Errorf("Expected 0 posts, got %d", len(posts))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hard delete with no posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
deletedCount, err := suite.PostRepo.HardDeleteAll()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if deletedCount != 0 {
|
||||
t.Errorf("Expected 0 posts deleted, got %d", deletedCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRepository_EdgeCases(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty URL", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser2", "test2@example.com", "password123")
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Test Post",
|
||||
URL: "",
|
||||
Content: "Test content",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err := suite.PostRepo.Create(post)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for empty URL: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid URL format", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser3", "test3@example.com", "password123")
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Test Post",
|
||||
URL: "not-a-url",
|
||||
Content: "Test content",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err := suite.PostRepo.Create(post)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for invalid URL format: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("very long title", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser4", "test4@example.com", "password123")
|
||||
|
||||
longTitle := strings.Repeat("a", 300)
|
||||
post := &database.Post{
|
||||
Title: longTitle,
|
||||
URL: "https://example.com",
|
||||
Content: "Test content",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err := suite.PostRepo.Create(post)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for long title: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
13
internal/repositories/refresh_token_interface.go
Normal file
13
internal/repositories/refresh_token_interface.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package repositories
|
||||
|
||||
import "goyco/internal/database"
|
||||
|
||||
type RefreshTokenRepositoryInterface interface {
|
||||
Create(token *database.RefreshToken) error
|
||||
GetByTokenHash(tokenHash string) (*database.RefreshToken, error)
|
||||
DeleteByUserID(userID uint) error
|
||||
DeleteExpired() error
|
||||
DeleteByID(id uint) error
|
||||
GetByUserID(userID uint) ([]database.RefreshToken, error)
|
||||
CountByUserID(userID uint) (int64, error)
|
||||
}
|
||||
55
internal/repositories/refresh_token_repository.go
Normal file
55
internal/repositories/refresh_token_repository.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
type RefreshTokenRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var _ RefreshTokenRepositoryInterface = (*RefreshTokenRepository)(nil)
|
||||
|
||||
func NewRefreshTokenRepository(db *gorm.DB) *RefreshTokenRepository {
|
||||
return &RefreshTokenRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepository) Create(token *database.RefreshToken) error {
|
||||
return r.db.Create(token).Error
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepository) GetByTokenHash(tokenHash string) (*database.RefreshToken, error) {
|
||||
var token database.RefreshToken
|
||||
err := r.db.Where("token_hash = ?", tokenHash).First(&token).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepository) DeleteByUserID(userID uint) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&database.RefreshToken{}).Error
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepository) DeleteExpired() error {
|
||||
return r.db.Where("expires_at < ?", time.Now()).Delete(&database.RefreshToken{}).Error
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepository) DeleteByID(id uint) error {
|
||||
return r.db.Delete(&database.RefreshToken{}, id).Error
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepository) GetByUserID(userID uint) ([]database.RefreshToken, error) {
|
||||
var tokens []database.RefreshToken
|
||||
err := r.db.Where("user_id = ?", userID).Find(&tokens).Error
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepository) CountByUserID(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&database.RefreshToken{}).Where("user_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
414
internal/repositories/refresh_token_repository_test.go
Normal file
414
internal/repositories/refresh_token_repository_test.go
Normal file
@@ -0,0 +1,414 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
func TestRefreshTokenRepository_Create(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-123",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
err := suite.RefreshTokenRepo.Create(token)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if token.ID == 0 {
|
||||
t.Error("Expected token ID to be assigned")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token with invalid user", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
token := &database.RefreshToken{
|
||||
UserID: 999,
|
||||
TokenHash: "token-hash-123",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
err := suite.RefreshTokenRepo.Create(token)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for invalid user (SQLite allows this): %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate token hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token1 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "duplicate-token",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
err := suite.RefreshTokenRepo.Create(token1)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error for first token, got %v", err)
|
||||
}
|
||||
|
||||
token2 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "duplicate-token",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
err = suite.RefreshTokenRepo.Create(token2)
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate token hash")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenRepository_GetByTokenHash(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing token", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-123",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
err := suite.RefreshTokenRepo.Create(token)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create token: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.RefreshTokenRepo.GetByTokenHash("token-hash-123")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected token, got nil")
|
||||
}
|
||||
|
||||
if retrieved.TokenHash != "token-hash-123" {
|
||||
t.Errorf("Expected token hash 'token-hash-123', got %s", retrieved.TokenHash)
|
||||
}
|
||||
if retrieved.UserID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, retrieved.UserID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existing token", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
_, err := suite.RefreshTokenRepo.GetByTokenHash("nonexistent-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existing token")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenRepository_DeleteByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful delete", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token1 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-1",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
token2 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-2",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
suite.RefreshTokenRepo.Create(token1)
|
||||
suite.RefreshTokenRepo.Create(token2)
|
||||
|
||||
err := suite.RefreshTokenRepo.DeleteByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.RefreshTokenRepo.GetByTokenHash("token-hash-1")
|
||||
if err == nil {
|
||||
t.Error("Expected error for deleted token")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete for user without tokens", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
err := suite.RefreshTokenRepo.DeleteByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Delete should succeed even for user without tokens (GORM behavior)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenRepository_DeleteExpired(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("delete expired tokens", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
expiredToken := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "expired-token",
|
||||
ExpiresAt: time.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
|
||||
validToken := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "valid-token",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
suite.RefreshTokenRepo.Create(expiredToken)
|
||||
suite.RefreshTokenRepo.Create(validToken)
|
||||
|
||||
err := suite.RefreshTokenRepo.DeleteExpired()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.RefreshTokenRepo.GetByTokenHash("expired-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for deleted expired token")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.RefreshTokenRepo.GetByTokenHash("valid-token")
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid token to still exist, got error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenRepository_DeleteByID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful delete", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-123",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
err := suite.RefreshTokenRepo.Create(token)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create token: %v", err)
|
||||
}
|
||||
|
||||
err = suite.RefreshTokenRepo.DeleteByID(token.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.RefreshTokenRepo.GetByTokenHash("token-hash-123")
|
||||
if err == nil {
|
||||
t.Error("Expected error for deleted token")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete non-existing token", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
err := suite.RefreshTokenRepo.DeleteByID(999)
|
||||
if err != nil {
|
||||
t.Fatalf("Delete should succeed even for non-existing token (GORM behavior)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenRepository_GetByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("user with tokens", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token1 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-1",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
token2 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-2",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
suite.RefreshTokenRepo.Create(token1)
|
||||
suite.RefreshTokenRepo.Create(token2)
|
||||
|
||||
tokens, err := suite.RefreshTokenRepo.GetByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(tokens) != 2 {
|
||||
t.Errorf("Expected 2 tokens, got %d", len(tokens))
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
if token.UserID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, token.UserID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user without tokens", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
tokens, err := suite.RefreshTokenRepo.GetByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(tokens) != 0 {
|
||||
t.Errorf("Expected 0 tokens, got %d", len(tokens))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenRepository_CountByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("user with tokens", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token1 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-1",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
token2 := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "token-hash-2",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
suite.RefreshTokenRepo.Create(token1)
|
||||
suite.RefreshTokenRepo.Create(token2)
|
||||
|
||||
count, err := suite.RefreshTokenRepo.CountByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 tokens, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user without tokens", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
count, err := suite.RefreshTokenRepo.CountByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 tokens, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenRepository_EdgeCases(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty token hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
token := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: "",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
err := suite.RefreshTokenRepo.Create(token)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for empty token hash: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero user ID", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
token := &database.RefreshToken{
|
||||
UserID: 0,
|
||||
TokenHash: "token-hash-123",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
err := suite.RefreshTokenRepo.Create(token)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for zero user ID: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("very long token hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
longToken := strings.Repeat("a", 300)
|
||||
token := &database.RefreshToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: longToken,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
err := suite.RefreshTokenRepo.Create(token)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for long token hash: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
165
internal/repositories/search_sanitizer.go
Normal file
165
internal/repositories/search_sanitizer.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type SearchSanitizer struct {
|
||||
MaxQueryLength int
|
||||
MaxSpecialChars int
|
||||
}
|
||||
|
||||
func NewSearchSanitizer() *SearchSanitizer {
|
||||
return &SearchSanitizer{
|
||||
MaxQueryLength: 100,
|
||||
MaxSpecialChars: 10,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SearchSanitizer) SanitizeSearchQuery(query string) (string, error) {
|
||||
query = strings.TrimSpace(query)
|
||||
|
||||
if query == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if len(query) > s.MaxQueryLength {
|
||||
return "", &SearchError{
|
||||
Type: "QueryTooLong",
|
||||
Message: "Search query exceeds maximum length",
|
||||
}
|
||||
}
|
||||
|
||||
specialCharCount := 0
|
||||
for _, char := range query {
|
||||
if !unicode.IsLetter(char) && !unicode.IsDigit(char) && !unicode.IsSpace(char) {
|
||||
specialCharCount++
|
||||
}
|
||||
}
|
||||
|
||||
if specialCharCount > s.MaxSpecialChars {
|
||||
return "", &SearchError{
|
||||
Type: "TooManySpecialChars",
|
||||
Message: "Search query contains too many special characters",
|
||||
}
|
||||
}
|
||||
|
||||
query = s.removeDangerousPatterns(query)
|
||||
|
||||
query = s.normalizeWhitespace(query)
|
||||
|
||||
if len(query) > s.MaxQueryLength {
|
||||
query = query[:s.MaxQueryLength]
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (s *SearchSanitizer) removeDangerousPatterns(query string) string {
|
||||
query = regexp.MustCompile(`\*{3,}`).ReplaceAllString(query, "**")
|
||||
query = regexp.MustCompile(`%{3,}`).ReplaceAllString(query, "%%")
|
||||
|
||||
query = regexp.MustCompile(`\.{3,}`).ReplaceAllString(query, "..")
|
||||
|
||||
query = regexp.MustCompile(`\?{3,}`).ReplaceAllString(query, "??")
|
||||
|
||||
query = regexp.MustCompile(`\+{3,}`).ReplaceAllString(query, "++")
|
||||
|
||||
query = regexp.MustCompile(`\{[^}]*\{[^}]*\}`).ReplaceAllString(query, "")
|
||||
|
||||
query = regexp.MustCompile(`\[[^\]]*\[[^\]]*\]`).ReplaceAllString(query, "")
|
||||
|
||||
query = regexp.MustCompile(`\([^)]*\([^)]*\)`).ReplaceAllString(query, "")
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (s *SearchSanitizer) normalizeWhitespace(query string) string {
|
||||
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
|
||||
|
||||
query = strings.TrimSpace(query)
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (s *SearchSanitizer) ValidateSearchQuery(query string) error {
|
||||
if len(query) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlInjectionPatterns := []string{
|
||||
"';", "--", "/*", "*/", "xp_", "sp_", "exec", "execute",
|
||||
"union", "select", "insert", "update", "delete", "drop",
|
||||
"create", "alter", "grant", "revoke", "truncate",
|
||||
}
|
||||
|
||||
lowerQuery := strings.ToLower(query)
|
||||
for _, pattern := range sqlInjectionPatterns {
|
||||
if strings.Contains(lowerQuery, pattern) {
|
||||
return &SearchError{
|
||||
Type: "InvalidQuery",
|
||||
Message: "Search query contains potentially dangerous patterns",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.hasExcessiveRepetition(query) {
|
||||
return &SearchError{
|
||||
Type: "DoSPattern",
|
||||
Message: "Search query contains patterns that could cause denial of service",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SearchSanitizer) hasExcessiveRepetition(query string) bool {
|
||||
if s.hasRepeatedCharacters(query, 5) {
|
||||
return true
|
||||
}
|
||||
|
||||
words := strings.Fields(query)
|
||||
wordCount := make(map[string]int)
|
||||
for _, word := range words {
|
||||
wordCount[strings.ToLower(word)]++
|
||||
if wordCount[strings.ToLower(word)] > 3 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SearchSanitizer) hasRepeatedCharacters(str string, maxRepeats int) bool {
|
||||
if len(str) <= maxRepeats {
|
||||
return false
|
||||
}
|
||||
|
||||
currentChar := rune(0)
|
||||
count := 0
|
||||
|
||||
for _, char := range str {
|
||||
if char == currentChar {
|
||||
count++
|
||||
if count > maxRepeats {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
currentChar = char
|
||||
count = 1
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type SearchError struct {
|
||||
Type string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *SearchError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
209
internal/repositories/search_sanitizer_test.go
Normal file
209
internal/repositories/search_sanitizer_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSearchSanitizer_SanitizeSearchQuery(t *testing.T) {
|
||||
sanitizer := NewSearchSanitizer()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
expectedResult string
|
||||
expectError bool
|
||||
errorType string
|
||||
}{
|
||||
{
|
||||
name: "normal_search",
|
||||
query: "golang programming",
|
||||
expectedResult: "golang programming",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty_query",
|
||||
query: "",
|
||||
expectedResult: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace_only",
|
||||
query: " ",
|
||||
expectedResult: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "normalize_whitespace",
|
||||
query: "golang programming tutorial",
|
||||
expectedResult: "golang programming tutorial",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "remove_excessive_wildcards",
|
||||
query: "test***wildcard",
|
||||
expectedResult: "test**wildcard",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "remove_excessive_dots",
|
||||
query: "test...dots",
|
||||
expectedResult: "test..dots",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "query_too_long",
|
||||
query: "a very long search query that exceeds the maximum length limit and should be rejected by the sanitizer",
|
||||
expectedResult: "",
|
||||
expectError: true,
|
||||
errorType: "QueryTooLong",
|
||||
},
|
||||
{
|
||||
name: "too_many_special_chars",
|
||||
query: "test@#$%^&*()_+{}|:<>?[]\\;'\",./",
|
||||
expectedResult: "",
|
||||
expectError: true,
|
||||
errorType: "TooManySpecialChars",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := sanitizer.SanitizeSearchQuery(tt.query)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
|
||||
if searchErr, ok := err.(*SearchError); ok {
|
||||
if searchErr.Type != tt.errorType {
|
||||
t.Errorf("Expected error type %s, got %s", tt.errorType, searchErr.Type)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Expected SearchError, got %T", err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("Expected result %q, got %q", tt.expectedResult, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchSanitizer_ValidateSearchQuery(t *testing.T) {
|
||||
sanitizer := NewSearchSanitizer()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
expectError bool
|
||||
errorType string
|
||||
}{
|
||||
{
|
||||
name: "valid_query",
|
||||
query: "golang programming",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty_query",
|
||||
query: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "sql_injection_attempt",
|
||||
query: "'; DROP TABLE users; --",
|
||||
expectError: true,
|
||||
errorType: "InvalidQuery",
|
||||
},
|
||||
{
|
||||
name: "union_attack",
|
||||
query: "test UNION SELECT * FROM users",
|
||||
expectError: true,
|
||||
errorType: "InvalidQuery",
|
||||
},
|
||||
{
|
||||
name: "excessive_repetition",
|
||||
query: "test test test test test",
|
||||
expectError: true,
|
||||
errorType: "DoSPattern",
|
||||
},
|
||||
{
|
||||
name: "repeated_characters",
|
||||
query: "aaaaaaaaaaaa",
|
||||
expectError: true,
|
||||
errorType: "DoSPattern",
|
||||
},
|
||||
{
|
||||
name: "valid_with_special_chars",
|
||||
query: "golang-programming_tutorial",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := sanitizer.ValidateSearchQuery(tt.query)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
|
||||
if searchErr, ok := err.(*SearchError); ok {
|
||||
if searchErr.Type != tt.errorType {
|
||||
t.Errorf("Expected error type %s, got %s", tt.errorType, searchErr.Type)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Expected SearchError, got %T", err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchSanitizer_EdgeCases(t *testing.T) {
|
||||
sanitizer := NewSearchSanitizer()
|
||||
|
||||
t.Run("unicode_characters", func(t *testing.T) {
|
||||
query := "golang программирование 🚀"
|
||||
result, err := sanitizer.SanitizeSearchQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if result != query {
|
||||
t.Errorf("Expected %q, got %q", query, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mixed_case_sql_injection", func(t *testing.T) {
|
||||
query := "test UnIoN SeLeCt * FrOm users"
|
||||
err := sanitizer.ValidateSearchQuery(query)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for SQL injection attempt")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("normalize_whitespace_complex", func(t *testing.T) {
|
||||
query := " \t\n golang \t\n programming \t\n "
|
||||
result, err := sanitizer.SanitizeSearchQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
expected := "golang programming"
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
306
internal/repositories/user_repository.go
Normal file
306
internal/repositories/user_repository.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/validation"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(user *database.User) error
|
||||
GetByID(id uint) (*database.User, error)
|
||||
GetByIDIncludingDeleted(id uint) (*database.User, error)
|
||||
GetByUsername(username string) (*database.User, error)
|
||||
GetByUsernameIncludingDeleted(username string) (*database.User, error)
|
||||
GetByEmail(email string) (*database.User, error)
|
||||
GetByVerificationToken(token string) (*database.User, error)
|
||||
GetByPasswordResetToken(token string) (*database.User, error)
|
||||
GetAll(limit, offset int) ([]database.User, error)
|
||||
Update(user *database.User) error
|
||||
Delete(id uint) error
|
||||
HardDelete(id uint) error
|
||||
SoftDeleteWithPosts(id uint) error
|
||||
Lock(id uint) error
|
||||
Unlock(id uint) error
|
||||
GetPosts(userID uint, limit, offset int) ([]database.Post, error)
|
||||
GetDeletedUsers() ([]database.User, error)
|
||||
HardDeleteAll() (int64, error)
|
||||
Count() (int64, error)
|
||||
WithTx(tx *gorm.DB) UserRepository
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(user *database.User) error {
|
||||
username := strings.TrimSpace(user.Username)
|
||||
if username == "" {
|
||||
return fmt.Errorf("username is required")
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(user.Email)
|
||||
if email == "" {
|
||||
return fmt.Errorf("email is required")
|
||||
}
|
||||
|
||||
if err := validation.ValidateEmail(email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
normalizedEmail := strings.ToLower(email)
|
||||
user.Email = normalizedEmail
|
||||
user.Username = username
|
||||
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByID(id uint) (*database.User, error) {
|
||||
var user database.User
|
||||
err := r.db.Preload("Posts").First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByIDIncludingDeleted(id uint) (*database.User, error) {
|
||||
var user database.User
|
||||
err := r.db.Unscoped().Preload("Posts").First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByUsername(username string) (*database.User, error) {
|
||||
var user database.User
|
||||
err := r.db.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByUsernameIncludingDeleted(username string) (*database.User, error) {
|
||||
var user database.User
|
||||
err := r.db.Unscoped().Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByEmail(email string) (*database.User, error) {
|
||||
var user database.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByVerificationToken(token string) (*database.User, error) {
|
||||
var user database.User
|
||||
err := r.db.Where("email_verification_token = ?", token).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByPasswordResetToken(token string) (*database.User, error) {
|
||||
var user database.User
|
||||
err := r.db.Where("password_reset_token = ?", token).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetAll(limit, offset int) ([]database.User, error) {
|
||||
var users []database.User
|
||||
query := r.db.Order("created_at DESC")
|
||||
query = ApplyPagination(query, limit, offset)
|
||||
|
||||
err := query.Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *userRepository) Update(user *database.User) error {
|
||||
if user == nil {
|
||||
return fmt.Errorf("user is nil")
|
||||
}
|
||||
|
||||
return r.db.Model(user).Select("*").Updates(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(id uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := deleteUserVotes(tx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&database.Post{}).Where("author_id = ?", id).Updates(map[string]any{
|
||||
"author_id": nil,
|
||||
"author_name": "(deleted)",
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("update user posts: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&database.AccountDeletionRequest{}).Error; err != nil {
|
||||
return fmt.Errorf("delete user deletion requests: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Delete(&database.User{}, id).Error; err != nil {
|
||||
return fmt.Errorf("delete user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *userRepository) HardDelete(id uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := deleteUserVotes(tx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Unscoped().Where("author_id = ?", id).Delete(&database.Post{}).Error; err != nil {
|
||||
return fmt.Errorf("delete user posts: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&database.AccountDeletionRequest{}).Error; err != nil {
|
||||
return fmt.Errorf("delete user deletion requests: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Unscoped().Delete(&database.User{}, id).Error; err != nil {
|
||||
return fmt.Errorf("delete user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *userRepository) SoftDeleteWithPosts(id uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := deleteUserVotes(tx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Unscoped().Model(&database.Post{}).Where("author_id = ?", id).Updates(map[string]any{
|
||||
"author_id": nil,
|
||||
"author_name": "(deleted)",
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("update user posts: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&database.AccountDeletionRequest{}).Error; err != nil {
|
||||
return fmt.Errorf("delete user deletion requests: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Unscoped().Delete(&database.User{}, id).Error; err != nil {
|
||||
return fmt.Errorf("delete user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *userRepository) GetPosts(userID uint, limit, offset int) ([]database.Post, error) {
|
||||
var posts []database.Post
|
||||
query := r.db.Where("author_id = ?", userID).Preload("Author").Order("created_at DESC")
|
||||
query = ApplyPagination(query, limit, offset)
|
||||
|
||||
err := query.Find(&posts).Error
|
||||
return posts, err
|
||||
}
|
||||
|
||||
func (r *userRepository) Lock(id uint) error {
|
||||
return r.db.Model(&database.User{}).Where("id = ?", id).Update("locked", true).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) Unlock(id uint) error {
|
||||
return r.db.Model(&database.User{}).Where("id = ?", id).Update("locked", false).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) GetDeletedUsers() ([]database.User, error) {
|
||||
var users []database.User
|
||||
err := r.db.Unscoped().
|
||||
Where("deleted_at IS NOT NULL").
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *userRepository) HardDeleteAll() (int64, error) {
|
||||
var totalDeleted int64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Unscoped().Where("1 = 1").Delete(&database.Vote{})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("delete all votes: %w", result.Error)
|
||||
}
|
||||
totalDeleted += result.RowsAffected
|
||||
|
||||
result = tx.Unscoped().Where("1 = 1").Delete(&database.Post{})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("delete all posts: %w", result.Error)
|
||||
}
|
||||
totalDeleted += result.RowsAffected
|
||||
|
||||
result = tx.Unscoped().Where("1 = 1").Delete(&database.AccountDeletionRequest{})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("delete all account deletion requests: %w", result.Error)
|
||||
}
|
||||
totalDeleted += result.RowsAffected
|
||||
|
||||
result = tx.Unscoped().Where("1 = 1").Delete(&database.User{})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("delete all users: %w", result.Error)
|
||||
}
|
||||
totalDeleted += result.RowsAffected
|
||||
|
||||
return nil
|
||||
})
|
||||
return totalDeleted, err
|
||||
}
|
||||
|
||||
func (r *userRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&database.User{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *userRepository) WithTx(tx *gorm.DB) UserRepository {
|
||||
return &userRepository{db: tx}
|
||||
}
|
||||
|
||||
func deleteUserVotes(tx *gorm.DB, userID uint) error {
|
||||
if err := tx.Unscoped().Where("user_id = ?", userID).Delete(&database.Vote{}).Error; err != nil {
|
||||
return fmt.Errorf("delete user votes: %w", err)
|
||||
}
|
||||
|
||||
var posts []database.Post
|
||||
if err := tx.Unscoped().Where("author_id = ?", userID).Find(&posts).Error; err != nil {
|
||||
return fmt.Errorf("get user posts: %w", err)
|
||||
}
|
||||
|
||||
if len(posts) > 0 {
|
||||
postIDs := make([]uint, len(posts))
|
||||
for i, post := range posts {
|
||||
postIDs[i] = post.ID
|
||||
}
|
||||
if err := tx.Unscoped().Where("post_id IN (?)", postIDs).Delete(&database.Vote{}).Error; err != nil {
|
||||
return fmt.Errorf("delete votes on user posts: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1259
internal/repositories/user_repository_test.go
Normal file
1259
internal/repositories/user_repository_test.go
Normal file
File diff suppressed because it is too large
Load Diff
146
internal/repositories/vote_repository.go
Normal file
146
internal/repositories/vote_repository.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
type VoteRepository interface {
|
||||
Create(vote *database.Vote) error
|
||||
CreateOrUpdate(vote *database.Vote) error
|
||||
GetByID(id uint) (*database.Vote, error)
|
||||
GetByUserAndPost(userID, postID uint) (*database.Vote, error)
|
||||
GetByVoteHash(voteHash string) (*database.Vote, error)
|
||||
GetByPostID(postID uint) ([]database.Vote, error)
|
||||
GetByUserID(userID uint) ([]database.Vote, error)
|
||||
Update(vote *database.Vote) error
|
||||
Delete(id uint) error
|
||||
Count() (int64, error)
|
||||
CountByPostID(postID uint) (int64, error)
|
||||
CountByUserID(userID uint) (int64, error)
|
||||
WithTx(tx *gorm.DB) VoteRepository
|
||||
}
|
||||
|
||||
type voteRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewVoteRepository(db *gorm.DB) VoteRepository {
|
||||
return &voteRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *voteRepository) Create(vote *database.Vote) error {
|
||||
return r.db.Create(vote).Error
|
||||
}
|
||||
|
||||
func (r *voteRepository) CreateOrUpdate(vote *database.Vote) error {
|
||||
var existingVote *database.Vote
|
||||
var err error
|
||||
var lookupByUserID bool
|
||||
|
||||
if vote.UserID != nil {
|
||||
existingVote, err = r.GetByUserAndPost(*vote.UserID, vote.PostID)
|
||||
lookupByUserID = true
|
||||
} else if vote.VoteHash != nil {
|
||||
existingVote, err = r.GetByVoteHash(*vote.VoteHash)
|
||||
lookupByUserID = false
|
||||
} else {
|
||||
return errors.New("vote must have either user_id or vote_hash")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err := r.Create(vote)
|
||||
if err != nil && errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
if lookupByUserID {
|
||||
existingVote, err = r.GetByUserAndPost(*vote.UserID, vote.PostID)
|
||||
} else {
|
||||
existingVote, err = r.GetByVoteHash(*vote.VoteHash)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existingVote.Type = vote.Type
|
||||
existingVote.UpdatedAt = vote.UpdatedAt
|
||||
return r.Update(existingVote)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
existingVote.Type = vote.Type
|
||||
existingVote.UpdatedAt = vote.UpdatedAt
|
||||
return r.Update(existingVote)
|
||||
}
|
||||
|
||||
func (r *voteRepository) GetByID(id uint) (*database.Vote, error) {
|
||||
var vote database.Vote
|
||||
err := r.db.Preload("User").Preload("Post").First(&vote, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vote, nil
|
||||
}
|
||||
|
||||
func (r *voteRepository) GetByUserAndPost(userID, postID uint) (*database.Vote, error) {
|
||||
var vote database.Vote
|
||||
err := r.db.Where("user_id = ? AND post_id = ?", userID, postID).First(&vote).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vote, nil
|
||||
}
|
||||
|
||||
func (r *voteRepository) GetByVoteHash(voteHash string) (*database.Vote, error) {
|
||||
var vote database.Vote
|
||||
err := r.db.Where("vote_hash = ?", voteHash).First(&vote).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vote, nil
|
||||
}
|
||||
|
||||
func (r *voteRepository) GetByPostID(postID uint) ([]database.Vote, error) {
|
||||
var votes []database.Vote
|
||||
err := r.db.Where("post_id = ?", postID).Find(&votes).Error
|
||||
return votes, err
|
||||
}
|
||||
|
||||
func (r *voteRepository) GetByUserID(userID uint) ([]database.Vote, error) {
|
||||
var votes []database.Vote
|
||||
err := r.db.Where("user_id = ?", userID).Preload("Post").Find(&votes).Error
|
||||
return votes, err
|
||||
}
|
||||
|
||||
func (r *voteRepository) Update(vote *database.Vote) error {
|
||||
return r.db.Save(vote).Error
|
||||
}
|
||||
|
||||
func (r *voteRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&database.Vote{}, id).Error
|
||||
}
|
||||
|
||||
func (r *voteRepository) CountByPostID(postID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&database.Vote{}).Where("post_id = ?", postID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *voteRepository) CountByUserID(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&database.Vote{}).Where("user_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *voteRepository) WithTx(tx *gorm.DB) VoteRepository {
|
||||
return &voteRepository{db: tx}
|
||||
}
|
||||
|
||||
func (r *voteRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&database.Vote{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
781
internal/repositories/vote_repository_test.go
Normal file
781
internal/repositories/vote_repository_test.go
Normal file
@@ -0,0 +1,781 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"goyco/internal/database"
|
||||
)
|
||||
|
||||
func TestVoteRepository_Create(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful creation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
if suite.GetVoteCount() != 1 {
|
||||
t.Errorf("Expected 1 vote, got %d", suite.GetVoteCount())
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("duplicate vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
vote2 := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteDown,
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote2)
|
||||
if err == nil {
|
||||
t.Error("Expected duplicate vote constraint error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("vote with invalid user", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: suite.CreateInvalidUserID(),
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for invalid user (SQLite allows this): %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("vote with invalid post", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: 999,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for invalid post (SQLite allows this): %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_GetByID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
vote := suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByID(vote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected vote, got nil")
|
||||
}
|
||||
|
||||
if retrieved.ID != vote.ID {
|
||||
t.Errorf("Expected ID %d, got %d", vote.ID, retrieved.ID)
|
||||
}
|
||||
if vote.UserID == nil {
|
||||
t.Fatal("Test setup error: vote.UserID is nil")
|
||||
}
|
||||
if retrieved.UserID == nil {
|
||||
t.Fatalf("Expected user ID %d, got nil", *vote.UserID)
|
||||
}
|
||||
if *retrieved.UserID != *vote.UserID {
|
||||
t.Errorf("Expected user ID %d, got %d", *vote.UserID, *retrieved.UserID)
|
||||
}
|
||||
if retrieved.PostID != vote.PostID {
|
||||
t.Errorf("Expected post ID %d, got %d", vote.PostID, retrieved.PostID)
|
||||
}
|
||||
if retrieved.Type != vote.Type {
|
||||
t.Errorf("Expected type %v, got %v", vote.Type, retrieved.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existing vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
_, err := suite.VoteRepo.GetByID(999)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existing vote")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_GetByUserAndPost(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByUserAndPost(user.ID, post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected vote, got nil")
|
||||
}
|
||||
|
||||
if retrieved.UserID == nil || *retrieved.UserID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, retrieved.UserID)
|
||||
}
|
||||
if retrieved.PostID != post.ID {
|
||||
t.Errorf("Expected post ID %d, got %d", post.ID, retrieved.PostID)
|
||||
}
|
||||
if retrieved.Type != database.VoteUp {
|
||||
t.Errorf("Expected type %v, got %v", database.VoteUp, retrieved.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existing vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
_, err := suite.VoteRepo.GetByUserAndPost(user.ID, post.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existing vote")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_GetByPostID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("post with votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
post := suite.CreateTestPost(user1.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
suite.CreateTestVote(user1.ID, post.ID, database.VoteUp)
|
||||
suite.CreateTestVote(user2.ID, post.ID, database.VoteDown)
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByPostID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 votes, got %d", len(retrieved))
|
||||
}
|
||||
|
||||
for _, vote := range retrieved {
|
||||
if vote.PostID != post.ID {
|
||||
t.Errorf("Expected post ID %d, got %d", post.ID, vote.PostID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("post without votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByPostID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 0 {
|
||||
t.Errorf("Expected 0 votes, got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_GetByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("user with votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post1 := suite.CreateTestPost(user.ID, "Post 1", "https://example.com/1", "Content 1")
|
||||
post2 := suite.CreateTestPost(user.ID, "Post 2", "https://example.com/2", "Content 2")
|
||||
|
||||
suite.CreateTestVote(user.ID, post1.ID, database.VoteUp)
|
||||
suite.CreateTestVote(user.ID, post2.ID, database.VoteDown)
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 2 {
|
||||
t.Errorf("Expected 2 votes, got %d", len(retrieved))
|
||||
}
|
||||
|
||||
for _, vote := range retrieved {
|
||||
if vote.UserID == nil || *vote.UserID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, vote.UserID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user without votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved) != 0 {
|
||||
t.Errorf("Expected 0 votes, got %d", len(retrieved))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_Update(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful update", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
vote := suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
vote.Type = database.VoteDown
|
||||
err := suite.VoteRepo.Update(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByID(vote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve vote: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Type != database.VoteDown {
|
||||
t.Errorf("Expected type %v, got %v", database.VoteDown, retrieved.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_Delete(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("successful delete", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
vote := suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
err := suite.VoteRepo.Delete(vote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.VoteRepo.GetByID(vote.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected error for deleted vote")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_CountByPostID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("post with votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
user3 := suite.CreateTestUser("user3", "user3@example.com", "password123")
|
||||
post := suite.CreateTestPost(user1.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
suite.CreateTestVote(user1.ID, post.ID, database.VoteUp)
|
||||
suite.CreateTestVote(user2.ID, post.ID, database.VoteDown)
|
||||
suite.CreateTestVote(user3.ID, post.ID, database.VoteUp)
|
||||
|
||||
count, err := suite.VoteRepo.CountByPostID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 3 {
|
||||
t.Errorf("Expected 3 votes, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("post without votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
count, err := suite.VoteRepo.CountByPostID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 votes, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_CountByUserID(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("user with votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post1 := suite.CreateTestPost(user.ID, "Post 1", "https://example.com/1", "Content 1")
|
||||
post2 := suite.CreateTestPost(user.ID, "Post 2", "https://example.com/2", "Content 2")
|
||||
|
||||
suite.CreateTestVote(user.ID, post1.ID, database.VoteUp)
|
||||
suite.CreateTestVote(user.ID, post2.ID, database.VoteDown)
|
||||
|
||||
count, err := suite.VoteRepo.CountByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 votes, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user without votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
count, err := suite.VoteRepo.CountByUserID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 votes, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_EdgeCases(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("invalid vote type", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: "invalid",
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for invalid vote type: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero user ID", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: nil,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for nil user ID: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero post ID", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: 0,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for zero post ID: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_CreateOrUpdate(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("create new vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
|
||||
err := suite.VoteRepo.CreateOrUpdate(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if vote.ID == 0 {
|
||||
t.Error("Expected vote ID to be assigned")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update existing vote", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
initialVote := suite.CreateTestVote(user.ID, post.ID, database.VoteUp)
|
||||
|
||||
updateVote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteDown,
|
||||
}
|
||||
|
||||
err := suite.VoteRepo.CreateOrUpdate(updateVote)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByID(initialVote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve vote: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Type != database.VoteDown {
|
||||
t.Errorf("Expected vote type %v, got %v", database.VoteDown, retrieved.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create vote with vote hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
post := suite.CreateTestPost(1, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
vote := &database.Vote{
|
||||
VoteHash: func() *string { s := "vote-hash-123"; return &s }(),
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
|
||||
err := suite.VoteRepo.CreateOrUpdate(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if vote.ID == 0 {
|
||||
t.Error("Expected vote ID to be assigned")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update vote with vote hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
post := suite.CreateTestPost(1, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
initialVote := &database.Vote{
|
||||
VoteHash: func() *string { s := "vote-hash-123"; return &s }(),
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err := suite.VoteRepo.Create(initialVote)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create initial vote: %v", err)
|
||||
}
|
||||
|
||||
updateVote := &database.Vote{
|
||||
VoteHash: func() *string { s := "vote-hash-123"; return &s }(),
|
||||
PostID: post.ID,
|
||||
Type: database.VoteDown,
|
||||
}
|
||||
|
||||
err = suite.VoteRepo.CreateOrUpdate(updateVote)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByVoteHash("vote-hash-123")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve vote: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Type != database.VoteDown {
|
||||
t.Errorf("Expected vote type %v, got %v", database.VoteDown, retrieved.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("vote without user_id or vote_hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
post := suite.CreateTestPost(1, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
vote := &database.Vote{
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
|
||||
err := suite.VoteRepo.CreateOrUpdate(vote)
|
||||
if err == nil {
|
||||
t.Error("Expected error for vote without user_id or vote_hash")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_GetByVoteHash(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("existing vote with hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
post := suite.CreateTestPost(1, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
vote := &database.Vote{
|
||||
VoteHash: func() *string { s := "vote-hash-123"; return &s }(),
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vote: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByVoteHash("vote-hash-123")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved == nil {
|
||||
t.Fatal("Expected vote, got nil")
|
||||
}
|
||||
|
||||
if retrieved.VoteHash == nil || *retrieved.VoteHash != "vote-hash-123" {
|
||||
t.Errorf("Expected vote hash 'vote-hash-123', got %v", retrieved.VoteHash)
|
||||
}
|
||||
if retrieved.PostID != post.ID {
|
||||
t.Errorf("Expected post ID %d, got %d", post.ID, retrieved.PostID)
|
||||
}
|
||||
if retrieved.Type != database.VoteUp {
|
||||
t.Errorf("Expected type %v, got %v", database.VoteUp, retrieved.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existing vote hash", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
_, err := suite.VoteRepo.GetByVoteHash("nonexistent-hash")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existing vote hash")
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Errorf("Expected ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_WithTx(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("transaction repository", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := suite.CreateTestUser("testuser", "test@example.com", "password123")
|
||||
post := suite.CreateTestPost(user.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
tx := suite.DB.Begin()
|
||||
defer tx.Rollback()
|
||||
|
||||
txVoteRepo := suite.VoteRepo.WithTx(tx)
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
|
||||
err := txVoteRepo.Create(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := txVoteRepo.GetByID(vote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to find vote in transaction, got %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Type != database.VoteUp {
|
||||
t.Errorf("Expected vote type %v, got %v", database.VoteUp, retrieved.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_Count(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
count, err := suite.VoteRepo.Count()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected count 0, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with votes", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
post := suite.CreateTestPost(user1.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
suite.CreateTestVote(user1.ID, post.ID, database.VoteUp)
|
||||
suite.CreateTestVote(user2.ID, post.ID, database.VoteDown)
|
||||
|
||||
count, err := suite.VoteRepo.Count()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if count != 2 {
|
||||
t.Errorf("Expected count 2, got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVoteRepository_ConcurrentAccess(t *testing.T) {
|
||||
suite := NewTestSuite(t)
|
||||
|
||||
t.Run("concurrent votes on same post with consistency check", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user1 := suite.CreateTestUser("user1", "user1@example.com", "password123")
|
||||
user2 := suite.CreateTestUser("user2", "user2@example.com", "password123")
|
||||
user3 := suite.CreateTestUser("user3", "user3@example.com", "password123")
|
||||
post := suite.CreateTestPost(user1.ID, "Test Post", "https://example.com", "Test content")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 3)
|
||||
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := suite.VoteRepo.Create(&database.Vote{UserID: &user1.ID, PostID: post.ID, Type: database.VoteUp}); err != nil {
|
||||
errors <- err
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := suite.VoteRepo.Create(&database.Vote{UserID: &user2.ID, PostID: post.ID, Type: database.VoteDown}); err != nil {
|
||||
errors <- err
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := suite.VoteRepo.Create(&database.Vote{UserID: &user3.ID, PostID: post.ID, Type: database.VoteUp}); err != nil {
|
||||
errors <- err
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
for err := range errors {
|
||||
t.Errorf("Concurrent vote creation failed: %v", err)
|
||||
}
|
||||
|
||||
count, err := suite.VoteRepo.CountByPostID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to count votes: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Errorf("Expected 3 votes, got %d", count)
|
||||
}
|
||||
|
||||
votes, err := suite.VoteRepo.GetByPostID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get votes: %v", err)
|
||||
}
|
||||
if len(votes) != 3 {
|
||||
t.Errorf("Expected 3 votes, got %d", len(votes))
|
||||
}
|
||||
|
||||
uniqueUsers := make(map[uint]bool)
|
||||
for _, vote := range votes {
|
||||
if vote.UserID != nil {
|
||||
if uniqueUsers[*vote.UserID] {
|
||||
t.Errorf("Duplicate vote from user %d", *vote.UserID)
|
||||
}
|
||||
uniqueUsers[*vote.UserID] = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user