mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 00:31:35 +00:00
Merge pull request #23 from SamyRai/test/increase-cache-coverage
test: Increase test coverage for internal/platform/cache
This commit is contained in:
commit
3dfe5986cf
1
go.mod
1
go.mod
@ -66,6 +66,7 @@ require (
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-redis/redismock/v9 v9.2.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@ -152,6 +152,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
|
||||
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
|
||||
113
internal/adapters/graphql/auth_mutations_test.go
Normal file
113
internal/adapters/graphql/auth_mutations_test.go
Normal file
@ -0,0 +1,113 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type AuthMutationTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
resolver graphql.MutationResolver
|
||||
}
|
||||
|
||||
func TestAuthMutations(t *testing.T) {
|
||||
suite.Run(t, new(AuthMutationTestSuite))
|
||||
}
|
||||
|
||||
func (s *AuthMutationTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(&testutil.TestConfig{
|
||||
DBPath: "auth_mutations_test.db",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthMutationTestSuite) TearDownSuite() {
|
||||
s.IntegrationTestSuite.TearDownSuite()
|
||||
os.Remove("auth_mutations_test.db")
|
||||
}
|
||||
|
||||
func (s *AuthMutationTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.resolver = (&graphql.Resolver{App: s.App}).Mutation()
|
||||
}
|
||||
|
||||
func (s *AuthMutationTestSuite) TestChangePassword() {
|
||||
// Helper to create a user for tests
|
||||
createUser := func(username, email, password string) *domain.User {
|
||||
resp, err := s.App.Auth.Commands.Register(context.Background(), auth.RegisterInput{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
return resp.User
|
||||
}
|
||||
|
||||
// Helper to create a context with JWT claims
|
||||
contextWithClaims := func(user *domain.User) context.Context {
|
||||
return testutil.ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: user.ID,
|
||||
Role: string(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Arrange
|
||||
initialPassword := "password123"
|
||||
newPassword := "newPassword456"
|
||||
user := createUser("testuser-changepw", "testuser.changepw@test.com", initialPassword)
|
||||
ctx := contextWithClaims(user)
|
||||
|
||||
// Act
|
||||
success, err := s.resolver.ChangePassword(ctx, initialPassword, newPassword)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(success)
|
||||
|
||||
// Verify the password change by trying to log in with the new and old passwords
|
||||
_, err = s.App.Auth.Commands.Login(context.Background(), auth.LoginInput{Email: user.Email, Password: newPassword})
|
||||
s.NoError(err, "Login with new password should succeed")
|
||||
|
||||
_, err = s.App.Auth.Commands.Login(context.Background(), auth.LoginInput{Email: user.Email, Password: initialPassword})
|
||||
s.Error(err, "Login with old password should fail")
|
||||
s.ErrorIs(err, auth.ErrInvalidCredentials)
|
||||
})
|
||||
|
||||
s.Run("Incorrect current password", func() {
|
||||
// Arrange
|
||||
initialPassword := "password123"
|
||||
newPassword := "newPassword456"
|
||||
user := createUser("testuser-wrongpw", "testuser.wrongpw@test.com", initialPassword)
|
||||
ctx := contextWithClaims(user)
|
||||
|
||||
// Act
|
||||
success, err := s.resolver.ChangePassword(ctx, "wrong-password", newPassword)
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.False(success)
|
||||
s.ErrorIs(err, auth.ErrInvalidCredentials)
|
||||
|
||||
// Verify the password was not changed
|
||||
_, loginErr := s.App.Auth.Commands.Login(context.Background(), auth.LoginInput{Email: user.Email, Password: initialPassword})
|
||||
s.NoError(loginErr, "Login with original password should still succeed")
|
||||
})
|
||||
|
||||
s.Run("Unauthenticated user", func() {
|
||||
// Act
|
||||
success, err := s.resolver.ChangePassword(context.Background(), "any-password", "any-new-password")
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.False(success)
|
||||
s.ErrorIs(err, domain.ErrUnauthorized)
|
||||
})
|
||||
}
|
||||
218
internal/adapters/graphql/user_mutations_test.go
Normal file
218
internal/adapters/graphql/user_mutations_test.go
Normal file
@ -0,0 +1,218 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type UserMutationTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
resolver graphql.MutationResolver
|
||||
}
|
||||
|
||||
func TestUserMutations(t *testing.T) {
|
||||
suite.Run(t, new(UserMutationTestSuite))
|
||||
}
|
||||
|
||||
func (s *UserMutationTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(&testutil.TestConfig{
|
||||
DBPath: "user_mutations_test.db",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserMutationTestSuite) TearDownSuite() {
|
||||
s.IntegrationTestSuite.TearDownSuite()
|
||||
os.Remove("user_mutations_test.db")
|
||||
}
|
||||
|
||||
func (s *UserMutationTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.resolver = (&graphql.Resolver{App: s.App}).Mutation()
|
||||
}
|
||||
|
||||
// Helper to create a user for tests
|
||||
func (s *UserMutationTestSuite) createUser(username, email, password string, role domain.UserRole) *domain.User {
|
||||
resp, err := s.App.Auth.Commands.Register(context.Background(), auth.RegisterInput{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
user, err := s.App.User.Queries.User(context.Background(), resp.User.ID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
if role != user.Role {
|
||||
user.Role = role
|
||||
err = s.DB.Save(user).Error
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// Helper to create a context with JWT claims
|
||||
func (s *UserMutationTestSuite) contextWithClaims(user *domain.User) context.Context {
|
||||
return testutil.ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: user.ID,
|
||||
Role: string(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserMutationTestSuite) TestDeleteUser() {
|
||||
s.Run("Success as admin", func() {
|
||||
// Arrange
|
||||
adminUser := s.createUser("admin_deleter", "admin_deleter@test.com", "password123", domain.UserRoleAdmin)
|
||||
userToDelete := s.createUser("user_to_delete", "user_to_delete@test.com", "password123", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(adminUser)
|
||||
userIDToDeleteStr := fmt.Sprintf("%d", userToDelete.ID)
|
||||
|
||||
// Act
|
||||
deleted, err := s.resolver.DeleteUser(ctx, userIDToDeleteStr)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(deleted)
|
||||
|
||||
// Verify user is deleted from DB
|
||||
_, err = s.App.User.Queries.User(context.Background(), userToDelete.ID)
|
||||
s.Require().Error(err)
|
||||
s.Contains(err.Error(), "entity not found", "Expected user to be not found after deletion")
|
||||
})
|
||||
|
||||
s.Run("Success as self", func() {
|
||||
// Arrange
|
||||
userToDelete := s.createUser("user_to_delete_self", "user_to_delete_self@test.com", "password123", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(userToDelete)
|
||||
userIDToDeleteStr := fmt.Sprintf("%d", userToDelete.ID)
|
||||
|
||||
// Act
|
||||
deleted, err := s.resolver.DeleteUser(ctx, userIDToDeleteStr)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(deleted)
|
||||
|
||||
// Verify user is deleted from DB
|
||||
_, err = s.App.User.Queries.User(context.Background(), userToDelete.ID)
|
||||
s.Require().Error(err)
|
||||
s.Contains(err.Error(), "entity not found", "Expected user to be not found after deletion")
|
||||
})
|
||||
|
||||
s.Run("Forbidden as other user", func() {
|
||||
// Arrange
|
||||
otherUser := s.createUser("other_user_deleter", "other_user_deleter@test.com", "password123", domain.UserRoleReader)
|
||||
userToDelete := s.createUser("user_to_be_kept", "user_to_be_kept@test.com", "password123", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(otherUser)
|
||||
userIDToDeleteStr := fmt.Sprintf("%d", userToDelete.ID)
|
||||
|
||||
// Act
|
||||
deleted, err := s.resolver.DeleteUser(ctx, userIDToDeleteStr)
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.False(deleted)
|
||||
s.True(errors.Is(err, domain.ErrForbidden))
|
||||
})
|
||||
|
||||
s.Run("Invalid user ID", func() {
|
||||
// Arrange
|
||||
adminUser := s.createUser("admin_deleter_2", "admin_deleter_2@test.com", "password123", domain.UserRoleAdmin)
|
||||
ctx := s.contextWithClaims(adminUser)
|
||||
|
||||
// Act
|
||||
deleted, err := s.resolver.DeleteUser(ctx, "invalid-id")
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.False(deleted)
|
||||
s.True(errors.Is(err, domain.ErrValidation))
|
||||
})
|
||||
|
||||
s.Run("User not found", func() {
|
||||
// Arrange
|
||||
adminUser := s.createUser("admin_deleter_3", "admin_deleter_3@test.com", "password123", domain.UserRoleAdmin)
|
||||
ctx := s.contextWithClaims(adminUser)
|
||||
nonExistentID := "999999"
|
||||
|
||||
// Act
|
||||
deleted, err := s.resolver.DeleteUser(ctx, nonExistentID)
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.False(deleted)
|
||||
s.Contains(err.Error(), "entity not found", "Expected entity not found error for non-existent user")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserMutationTestSuite) TestUpdateProfile() {
|
||||
s.Run("Success", func() {
|
||||
// Arrange
|
||||
user := s.createUser("profile_user", "profile.user@test.com", "password123", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
newFirstName := "John"
|
||||
newLastName := "Doe"
|
||||
newBio := "This is my new bio."
|
||||
input := model.UserInput{
|
||||
FirstName: &newFirstName,
|
||||
LastName: &newLastName,
|
||||
Bio: &newBio,
|
||||
}
|
||||
|
||||
// Act
|
||||
updatedUser, err := s.resolver.UpdateProfile(ctx, input)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(updatedUser)
|
||||
s.Equal(newFirstName, *updatedUser.FirstName)
|
||||
s.Equal(newLastName, *updatedUser.LastName)
|
||||
s.Equal(newBio, *updatedUser.Bio)
|
||||
|
||||
// Verify in DB
|
||||
dbUser, err := s.App.User.Queries.User(context.Background(), user.ID)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(newFirstName, dbUser.FirstName)
|
||||
s.Equal(newLastName, dbUser.LastName)
|
||||
s.Equal(newBio, dbUser.Bio)
|
||||
})
|
||||
|
||||
s.Run("Unauthenticated user", func() {
|
||||
// Arrange
|
||||
newFirstName := "Jane"
|
||||
input := model.UserInput{FirstName: &newFirstName}
|
||||
|
||||
// Act
|
||||
_, err := s.resolver.UpdateProfile(context.Background(), input)
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrUnauthorized)
|
||||
})
|
||||
|
||||
s.Run("Invalid country ID", func() {
|
||||
// Arrange
|
||||
user := s.createUser("profile_user_invalid", "profile.user.invalid@test.com", "password123", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(user)
|
||||
invalidCountryID := "not-a-number"
|
||||
input := model.UserInput{CountryID: &invalidCountryID}
|
||||
|
||||
// Act
|
||||
_, err := s.resolver.UpdateProfile(ctx, input)
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.Contains(err.Error(), "invalid country ID")
|
||||
})
|
||||
}
|
||||
@ -40,6 +40,7 @@ type Service interface {
|
||||
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error
|
||||
UpdateTrending(ctx context.Context) error
|
||||
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error)
|
||||
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
@ -314,6 +315,12 @@ func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit
|
||||
return s.repo.GetTrendingWorks(ctx, timePeriod, limit)
|
||||
}
|
||||
|
||||
func (s *service) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateWorkStats")
|
||||
defer span.End()
|
||||
return s.repo.UpdateWorkStats(ctx, workID, stats)
|
||||
}
|
||||
|
||||
func (s *service) UpdateTrending(ctx context.Context) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateTrending")
|
||||
defer span.End()
|
||||
|
||||
@ -305,14 +305,18 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store the original ID to delete later, as the sourceStats.ID might be overwritten.
|
||||
originalSourceStatsID := sourceStats.ID
|
||||
|
||||
var targetStats domain.WorkStats
|
||||
err = tx.Where("work_id = ?", targetWorkID).First(&targetStats).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// If target has no stats, create new ones based on source stats.
|
||||
sourceStats.ID = 0 // Let GORM create a new record
|
||||
sourceStats.WorkID = targetWorkID
|
||||
if err = tx.Create(&sourceStats).Error; err != nil {
|
||||
// If target has no stats, create a new stats record for it.
|
||||
newStats := sourceStats
|
||||
newStats.ID = 0
|
||||
newStats.WorkID = targetWorkID
|
||||
if err = tx.Create(&newStats).Error; err != nil {
|
||||
return fmt.Errorf("failed to create new target stats: %w", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
@ -325,8 +329,8 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the old source stats
|
||||
if err = tx.Delete(&domain.WorkStats{}, sourceStats.ID).Error; err != nil {
|
||||
// Delete the old source stats using the stored original ID.
|
||||
if err = tx.Delete(&domain.WorkStats{}, originalSourceStatsID).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete source work stats: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ func (s *WorkCommandsSuite) TestCreateWork_RepoError() {
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestUpdateWork_Success() {
|
||||
ctx := platform_auth.ContextWithAdminUser(context.Background(), 1)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
|
||||
@ -111,17 +111,40 @@ func (s *WorkCommandsSuite) TestUpdateWork_EmptyLanguage() {
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestUpdateWork_RepoError() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
s.repo.updateFunc = func(ctx context.Context, w *domain.Work) error {
|
||||
return errors.New("db error")
|
||||
}
|
||||
err := s.commands.UpdateWork(context.Background(), work)
|
||||
err := s.commands.UpdateWork(ctx, work)
|
||||
assert.Error(s.T(), err)
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestUpdateWork_Forbidden() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 2, Role: string(domain.UserRoleReader)}) // Not an admin
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
|
||||
s.repo.isAuthorFunc = func(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
return false, nil // User is not an author
|
||||
}
|
||||
|
||||
err := s.commands.UpdateWork(ctx, work)
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrForbidden))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestUpdateWork_Unauthorized() {
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
err := s.commands.UpdateWork(context.Background(), work) // No user in context
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrUnauthorized))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestDeleteWork_Success() {
|
||||
ctx := platform_auth.ContextWithAdminUser(context.Background(), 1)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
|
||||
@ -142,13 +165,27 @@ func (s *WorkCommandsSuite) TestDeleteWork_ZeroID() {
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestDeleteWork_RepoError() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
s.repo.deleteFunc = func(ctx context.Context, id uint) error {
|
||||
return errors.New("db error")
|
||||
}
|
||||
err := s.commands.DeleteWork(context.Background(), 1)
|
||||
err := s.commands.DeleteWork(ctx, 1)
|
||||
assert.Error(s.T(), err)
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestDeleteWork_Forbidden() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 2, Role: string(domain.UserRoleReader)}) // Not an admin
|
||||
err := s.commands.DeleteWork(ctx, 1)
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrForbidden))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestDeleteWork_Unauthorized() {
|
||||
err := s.commands.DeleteWork(context.Background(), 1) // No user in context
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrUnauthorized))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestAnalyzeWork_Success() {
|
||||
work := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
|
||||
@ -221,82 +258,157 @@ func TestMergeWork_Integration(t *testing.T) {
|
||||
analyticsSvc := &mockAnalyticsService{}
|
||||
commands := NewWorkCommands(workRepo, searchClient, authzSvc, analyticsSvc)
|
||||
|
||||
// Provide a realistic implementation for the GetOrCreateWorkStats mock
|
||||
analyticsSvc.getOrCreateWorkStatsFunc = func(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||
var stats domain.WorkStats
|
||||
if err := db.Where(domain.WorkStats{WorkID: workID}).FirstOrCreate(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// --- Seed Data ---
|
||||
author1 := &domain.Author{Name: "Author One"}
|
||||
db.Create(author1)
|
||||
author2 := &domain.Author{Name: "Author Two"}
|
||||
db.Create(author2)
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
author1 := &domain.Author{Name: "Author One"}
|
||||
db.Create(author1)
|
||||
author2 := &domain.Author{Name: "Author Two"}
|
||||
db.Create(author2)
|
||||
|
||||
tag1 := &domain.Tag{Name: "Tag One"}
|
||||
db.Create(tag1)
|
||||
tag2 := &domain.Tag{Name: "Tag Two"}
|
||||
db.Create(tag2)
|
||||
tag1 := &domain.Tag{Name: "Tag One"}
|
||||
db.Create(tag1)
|
||||
tag2 := &domain.Tag{Name: "Tag Two"}
|
||||
db.Create(tag2)
|
||||
|
||||
sourceWork := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
||||
Title: "Source Work",
|
||||
Authors: []*domain.Author{author1},
|
||||
Tags: []*domain.Tag{tag1},
|
||||
}
|
||||
db.Create(sourceWork)
|
||||
db.Create(&domain.Translation{Title: "Source English", Language: "en", TranslatableID: sourceWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.Translation{Title: "Source French", Language: "fr", TranslatableID: sourceWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.WorkStats{WorkID: sourceWork.ID, Views: 10, Likes: 5})
|
||||
|
||||
targetWork := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
||||
Title: "Target Work",
|
||||
Authors: []*domain.Author{author2},
|
||||
Tags: []*domain.Tag{tag2},
|
||||
}
|
||||
db.Create(targetWork)
|
||||
db.Create(&domain.Translation{Title: "Target English", Language: "en", TranslatableID: targetWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.WorkStats{WorkID: targetWork.ID, Views: 20, Likes: 10})
|
||||
|
||||
// --- Execute Merge ---
|
||||
ctx := platform_auth.ContextWithAdminUser(context.Background(), 1)
|
||||
err = commands.MergeWork(ctx, sourceWork.ID, targetWork.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// --- Assertions ---
|
||||
// 1. Source work should be deleted
|
||||
var deletedWork domain.Work
|
||||
err = db.First(&deletedWork, sourceWork.ID).Error
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
|
||||
// 2. Target work should have merged data
|
||||
var finalTargetWork domain.Work
|
||||
db.Preload("Translations").Preload("Authors").Preload("Tags").First(&finalTargetWork, targetWork.ID)
|
||||
|
||||
assert.Len(t, finalTargetWork.Translations, 2, "Should have two translations after merge")
|
||||
foundEn := false
|
||||
foundFr := false
|
||||
for _, tr := range finalTargetWork.Translations {
|
||||
if tr.Language == "en" {
|
||||
foundEn = true
|
||||
assert.Equal(t, "Target English", tr.Title, "Should keep target's English translation")
|
||||
sourceWork := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
||||
Title: "Source Work",
|
||||
Authors: []*domain.Author{author1},
|
||||
Tags: []*domain.Tag{tag1},
|
||||
}
|
||||
if tr.Language == "fr" {
|
||||
foundFr = true
|
||||
assert.Equal(t, "Source French", tr.Title, "Should merge source's French translation")
|
||||
db.Create(sourceWork)
|
||||
db.Create(&domain.Translation{Title: "Source English", Language: "en", TranslatableID: sourceWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.Translation{Title: "Source French", Language: "fr", TranslatableID: sourceWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.WorkStats{WorkID: sourceWork.ID, Views: 10, Likes: 5})
|
||||
|
||||
targetWork := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
||||
Title: "Target Work",
|
||||
Authors: []*domain.Author{author2},
|
||||
Tags: []*domain.Tag{tag2},
|
||||
}
|
||||
}
|
||||
assert.True(t, foundEn, "English translation should be present")
|
||||
assert.True(t, foundFr, "French translation should be present")
|
||||
db.Create(targetWork)
|
||||
db.Create(&domain.Translation{Title: "Target English", Language: "en", TranslatableID: targetWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.WorkStats{WorkID: targetWork.ID, Views: 20, Likes: 10})
|
||||
|
||||
assert.Len(t, finalTargetWork.Authors, 2, "Authors should be merged")
|
||||
assert.Len(t, finalTargetWork.Tags, 2, "Tags should be merged")
|
||||
// --- Execute Merge ---
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
err = commands.MergeWork(ctx, sourceWork.ID, targetWork.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 3. Stats should be merged
|
||||
var finalStats domain.WorkStats
|
||||
db.Where("work_id = ?", targetWork.ID).First(&finalStats)
|
||||
assert.Equal(t, int64(30), finalStats.Views, "Views should be summed")
|
||||
assert.Equal(t, int64(15), finalStats.Likes, "Likes should be summed")
|
||||
// --- Assertions ---
|
||||
// 1. Source work should be deleted
|
||||
var deletedWork domain.Work
|
||||
err = db.First(&deletedWork, sourceWork.ID).Error
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
|
||||
// 4. Source stats should be deleted
|
||||
var deletedStats domain.WorkStats
|
||||
err = db.First(&deletedStats, "work_id = ?", sourceWork.ID).Error
|
||||
assert.Error(t, err, "Source stats should be deleted")
|
||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
// 2. Target work should have merged data
|
||||
var finalTargetWork domain.Work
|
||||
db.Preload("Translations").Preload("Authors").Preload("Tags").First(&finalTargetWork, targetWork.ID)
|
||||
|
||||
assert.Len(t, finalTargetWork.Translations, 2, "Should have two translations after merge")
|
||||
foundEn := false
|
||||
foundFr := false
|
||||
for _, tr := range finalTargetWork.Translations {
|
||||
if tr.Language == "en" {
|
||||
foundEn = true
|
||||
assert.Equal(t, "Target English", tr.Title, "Should keep target's English translation")
|
||||
}
|
||||
if tr.Language == "fr" {
|
||||
foundFr = true
|
||||
assert.Equal(t, "Source French", tr.Title, "Should merge source's French translation")
|
||||
}
|
||||
}
|
||||
assert.True(t, foundEn, "English translation should be present")
|
||||
assert.True(t, foundFr, "French translation should be present")
|
||||
|
||||
assert.Len(t, finalTargetWork.Authors, 2, "Authors should be merged")
|
||||
assert.Len(t, finalTargetWork.Tags, 2, "Tags should be merged")
|
||||
|
||||
// 3. Stats should be merged
|
||||
var finalStats domain.WorkStats
|
||||
db.Where("work_id = ?", targetWork.ID).First(&finalStats)
|
||||
assert.Equal(t, int64(30), finalStats.Views, "Views should be summed")
|
||||
assert.Equal(t, int64(15), finalStats.Likes, "Likes should be summed")
|
||||
|
||||
// 4. Source stats should be deleted
|
||||
var deletedStats domain.WorkStats
|
||||
err = db.First(&deletedStats, "work_id = ?", sourceWork.ID).Error
|
||||
assert.Error(t, err, "Source stats should be deleted")
|
||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
})
|
||||
|
||||
t.Run("Success with no target stats", func(t *testing.T) {
|
||||
sourceWork := &domain.Work{Title: "Source with Stats"}
|
||||
db.Create(sourceWork)
|
||||
db.Create(&domain.WorkStats{WorkID: sourceWork.ID, Views: 15, Likes: 7})
|
||||
|
||||
targetWork := &domain.Work{Title: "Target without Stats"}
|
||||
db.Create(targetWork)
|
||||
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
err := commands.MergeWork(ctx, sourceWork.ID, targetWork.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var finalStats domain.WorkStats
|
||||
db.Where("work_id = ?", targetWork.ID).First(&finalStats)
|
||||
assert.Equal(t, int64(15), finalStats.Views)
|
||||
assert.Equal(t, int64(7), finalStats.Likes)
|
||||
})
|
||||
|
||||
t.Run("Forbidden for non-admin", func(t *testing.T) {
|
||||
sourceWork := &domain.Work{Title: "Forbidden Source"}
|
||||
db.Create(sourceWork)
|
||||
targetWork := &domain.Work{Title: "Forbidden Target"}
|
||||
db.Create(targetWork)
|
||||
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 2, Role: string(domain.UserRoleReader)})
|
||||
err := commands.MergeWork(ctx, sourceWork.ID, targetWork.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, domain.ErrForbidden))
|
||||
})
|
||||
|
||||
t.Run("Source work not found", func(t *testing.T) {
|
||||
targetWork := &domain.Work{Title: "Existing Target"}
|
||||
db.Create(targetWork)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
|
||||
err := commands.MergeWork(ctx, 99999, targetWork.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "entity not found")
|
||||
})
|
||||
|
||||
t.Run("Target work not found", func(t *testing.T) {
|
||||
sourceWork := &domain.Work{Title: "Existing Source"}
|
||||
db.Create(sourceWork)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
|
||||
err := commands.MergeWork(ctx, sourceWork.ID, 99999)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "entity not found")
|
||||
})
|
||||
|
||||
t.Run("Cannot merge work into itself", func(t *testing.T) {
|
||||
work := &domain.Work{Title: "Self Merge Work"}
|
||||
db.Create(work)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
|
||||
err := commands.MergeWork(ctx, work.ID, work.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "source and target work IDs cannot be the same")
|
||||
})
|
||||
}
|
||||
@ -18,6 +18,7 @@ type mockWorkRepository struct {
|
||||
findByCategoryFunc func(ctx context.Context, categoryID uint) ([]domain.Work, error)
|
||||
findByLanguageFunc func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error)
|
||||
isAuthorFunc func(ctx context.Context, workID uint, authorID uint) (bool, error)
|
||||
listByCollectionIDFunc func(ctx context.Context, collectionID uint) ([]domain.Work, error)
|
||||
}
|
||||
|
||||
func (m *mockWorkRepository) IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
@ -57,6 +58,13 @@ func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*dom
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
if m.listByCollectionIDFunc != nil {
|
||||
return m.listByCollectionIDFunc(ctx, collectionID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
if m.getWithTranslationsFunc != nil {
|
||||
return m.getWithTranslationsFunc(ctx, id)
|
||||
|
||||
@ -11,6 +11,15 @@ type mockAnalyticsService struct {
|
||||
updateWorkSentimentFunc func(ctx context.Context, workID uint) error
|
||||
updateTranslationReadingTimeFunc func(ctx context.Context, translationID uint) error
|
||||
updateTranslationSentimentFunc func(ctx context.Context, translationID uint) error
|
||||
getOrCreateWorkStatsFunc func(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
||||
updateWorkStatsFunc func(ctx context.Context, workID uint, stats domain.WorkStats) error
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||
if m.updateWorkStatsFunc != nil {
|
||||
return m.updateWorkStatsFunc(ctx, workID, stats)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||
@ -78,6 +87,9 @@ func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, t
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||
if m.getOrCreateWorkStatsFunc != nil {
|
||||
return m.getOrCreateWorkStatsFunc(ctx, workID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||
|
||||
@ -45,6 +45,22 @@ func (s *WorkQueriesSuite) TestGetWorkByID_ZeroID() {
|
||||
assert.Nil(s.T(), w)
|
||||
}
|
||||
|
||||
func (s *WorkQueriesSuite) TestListByCollectionID_Success() {
|
||||
works := []domain.Work{{Title: "Test Work"}}
|
||||
s.repo.listByCollectionIDFunc = func(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
return works, nil
|
||||
}
|
||||
w, err := s.queries.ListByCollectionID(context.Background(), 1)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), works, w)
|
||||
}
|
||||
|
||||
func (s *WorkQueriesSuite) TestListByCollectionID_ZeroID() {
|
||||
w, err := s.queries.ListByCollectionID(context.Background(), 0)
|
||||
assert.Error(s.T(), err)
|
||||
assert.Nil(s.T(), w)
|
||||
}
|
||||
|
||||
func (s *WorkQueriesSuite) TestListWorks_Success() {
|
||||
domainWorks := &domain.PaginatedResult[domain.Work]{
|
||||
Items: []domain.Work{
|
||||
|
||||
24
internal/app/work/service_test.go
Normal file
24
internal/app/work/service_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package work
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"tercul/internal/app/authz"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
// Arrange
|
||||
mockRepo := &mockWorkRepository{}
|
||||
mockSearchClient := &mockSearchClient{}
|
||||
mockAuthzSvc := &authz.Service{}
|
||||
mockAnalyticsSvc := &mockAnalyticsService{}
|
||||
|
||||
// Act
|
||||
service := NewService(mockRepo, mockSearchClient, mockAuthzSvc, mockAnalyticsSvc)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, service, "The new service should not be nil")
|
||||
assert.NotNil(t, service.Commands, "The service Commands should not be nil")
|
||||
assert.NotNil(t, service.Queries, "The service Queries should not be nil")
|
||||
}
|
||||
204
internal/data/sql/analytics_repository_test.go
Normal file
204
internal/data/sql/analytics_repository_test.go
Normal file
@ -0,0 +1,204 @@
|
||||
package sql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// newTestAnalyticsRepoWithSQLite sets up an in-memory SQLite database for testing.
|
||||
func newTestAnalyticsRepoWithSQLite(t *testing.T) (analytics.Repository, *gorm.DB) {
|
||||
// Using "file::memory:?cache=shared" to ensure the in-memory database is shared across connections in the same process.
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Auto-migrate the necessary schemas
|
||||
err = db.AutoMigrate(
|
||||
&domain.Work{},
|
||||
&domain.WorkStats{},
|
||||
&domain.Translation{},
|
||||
&domain.TranslationStats{},
|
||||
&domain.Trending{},
|
||||
&domain.UserEngagement{},
|
||||
&domain.User{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &config.Config{}
|
||||
repo := sql.NewAnalyticsRepository(db, cfg)
|
||||
|
||||
// Clean up the database after the test
|
||||
t.Cleanup(func() {
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
err = sqlDB.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
return repo, db
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_IncrementWorkCounter(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create a work to associate stats with
|
||||
work := domain.Work{Title: "Test Work"}
|
||||
require.NoError(t, db.Create(&work).Error)
|
||||
|
||||
t.Run("creates_new_stats_if_not_exist", func(t *testing.T) {
|
||||
err := repo.IncrementWorkCounter(ctx, work.ID, "views", 5)
|
||||
require.NoError(t, err)
|
||||
|
||||
var stats domain.WorkStats
|
||||
err = db.Where("work_id = ?", work.ID).First(&stats).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(5), stats.Views)
|
||||
})
|
||||
|
||||
t.Run("increments_existing_stats", func(t *testing.T) {
|
||||
// Increment again
|
||||
err := repo.IncrementWorkCounter(ctx, work.ID, "views", 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
var stats domain.WorkStats
|
||||
err = db.Where("work_id = ?", work.ID).First(&stats).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(8), stats.Views) // 5 + 3
|
||||
})
|
||||
|
||||
t.Run("invalid_field", func(t *testing.T) {
|
||||
err := repo.IncrementWorkCounter(ctx, work.ID, "invalid_field", 1)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_GetTrendingWorks(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create some works and trending data
|
||||
work1 := domain.Work{Title: "Trending Work 1"}
|
||||
work2 := domain.Work{Title: "Trending Work 2"}
|
||||
require.NoError(t, db.Create(&work1).Error)
|
||||
require.NoError(t, db.Create(&work2).Error)
|
||||
|
||||
trendingData := []*domain.Trending{
|
||||
{EntityID: work1.ID, EntityType: "Work", Rank: 1, TimePeriod: "daily"},
|
||||
{EntityID: work2.ID, EntityType: "Work", Rank: 2, TimePeriod: "daily"},
|
||||
}
|
||||
require.NoError(t, db.Create(&trendingData).Error)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
works, err := repo.GetTrendingWorks(ctx, "daily", 5)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, works, 2)
|
||||
assert.Equal(t, work1.ID, works[0].ID)
|
||||
assert.Equal(t, work2.ID, works[1].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_UpdateTrendingWorks(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create an old trending record
|
||||
oldTrending := domain.Trending{EntityID: 99, EntityType: "Work", Rank: 1, TimePeriod: "daily"}
|
||||
require.NoError(t, db.Create(&oldTrending).Error)
|
||||
|
||||
newTrendingData := []*domain.Trending{
|
||||
{EntityID: 1, EntityType: "Work", Rank: 1, Score: 100, TimePeriod: "daily"},
|
||||
{EntityID: 2, EntityType: "Work", Rank: 2, Score: 90, TimePeriod: "daily"},
|
||||
}
|
||||
|
||||
err := repo.UpdateTrendingWorks(ctx, "daily", newTrendingData)
|
||||
require.NoError(t, err)
|
||||
|
||||
var trendingResult []domain.Trending
|
||||
db.Where("time_period = ?", "daily").Order("rank asc").Find(&trendingResult)
|
||||
|
||||
require.Len(t, trendingResult, 2)
|
||||
assert.Equal(t, uint(1), trendingResult[0].EntityID)
|
||||
assert.Equal(t, uint(2), trendingResult[1].EntityID)
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_GetOrCreate(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
work := domain.Work{Title: "Test Work"}
|
||||
require.NoError(t, db.Create(&work).Error)
|
||||
translation := domain.Translation{Title: "Test Translation", TranslatableID: work.ID, TranslatableType: "Work"}
|
||||
require.NoError(t, db.Create(&translation).Error)
|
||||
user := domain.User{Username: "testuser", Email: "test@test.com"}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
t.Run("GetOrCreateWorkStats", func(t *testing.T) {
|
||||
// Create
|
||||
stats, err := repo.GetOrCreateWorkStats(ctx, work.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, work.ID, stats.WorkID)
|
||||
|
||||
// Get
|
||||
stats2, err := repo.GetOrCreateWorkStats(ctx, work.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, stats.ID, stats2.ID)
|
||||
})
|
||||
|
||||
t.Run("GetOrCreateTranslationStats", func(t *testing.T) {
|
||||
// Create
|
||||
stats, err := repo.GetOrCreateTranslationStats(ctx, translation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, translation.ID, stats.TranslationID)
|
||||
|
||||
// Get
|
||||
stats2, err := repo.GetOrCreateTranslationStats(ctx, translation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, stats.ID, stats2.ID)
|
||||
})
|
||||
|
||||
t.Run("GetOrCreateUserEngagement", func(t *testing.T) {
|
||||
date := time.Now().Truncate(24 * time.Hour)
|
||||
// Create
|
||||
eng, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, eng.UserID)
|
||||
|
||||
// Get
|
||||
eng2, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, eng.ID, eng2.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_UpdateUserEngagement(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := domain.User{Username: "testuser", Email: "test@test.com"}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
date := time.Now().Truncate(24 * time.Hour)
|
||||
engagement, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date)
|
||||
require.NoError(t, err)
|
||||
|
||||
engagement.LikesGiven = 15
|
||||
engagement.WorksRead = 10
|
||||
|
||||
err = repo.UpdateUserEngagement(ctx, engagement)
|
||||
require.NoError(t, err)
|
||||
|
||||
var updatedEngagement domain.UserEngagement
|
||||
db.First(&updatedEngagement, engagement.ID)
|
||||
|
||||
assert.Equal(t, 15, updatedEngagement.LikesGiven)
|
||||
assert.Equal(t, 10, updatedEngagement.WorksRead)
|
||||
}
|
||||
91
internal/data/sql/auth_repository_test.go
Normal file
91
internal/data/sql/auth_repository_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
package sql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// newTestAuthRepoWithSQLite sets up an in-memory SQLite database for auth repository testing.
|
||||
func newTestAuthRepoWithSQLite(t *testing.T) (domain.AuthRepository, *gorm.DB) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Auto-migrate the necessary schemas
|
||||
err = db.AutoMigrate(
|
||||
&domain.User{},
|
||||
&domain.UserSession{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &config.Config{}
|
||||
repo := sql.NewAuthRepository(db, cfg)
|
||||
|
||||
// Clean up the database after the test
|
||||
t.Cleanup(func() {
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
err = sqlDB.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
return repo, db
|
||||
}
|
||||
|
||||
func TestAuthRepository_StoreToken(t *testing.T) {
|
||||
repo, db := newTestAuthRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create a user
|
||||
user := domain.User{Username: "authuser", Email: "auth@test.com"}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
token := "my-secret-token"
|
||||
expiresAt := time.Now().Add(1 * time.Hour)
|
||||
|
||||
err := repo.StoreToken(ctx, user.ID, token, expiresAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
var session domain.UserSession
|
||||
err = db.Where("token = ?", token).First(&session).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, user.ID, session.UserID)
|
||||
assert.Equal(t, token, session.Token)
|
||||
// Truncate to a reasonable precision for comparison
|
||||
assert.WithinDuration(t, expiresAt, session.ExpiresAt, time.Second)
|
||||
}
|
||||
|
||||
func TestAuthRepository_DeleteToken(t *testing.T) {
|
||||
repo, db := newTestAuthRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create a user and a session
|
||||
user := domain.User{Username: "authuser2", Email: "auth2@test.com"}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
token := "token-to-delete"
|
||||
session := &domain.UserSession{
|
||||
UserID: user.ID,
|
||||
Token: token,
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour),
|
||||
}
|
||||
require.NoError(t, db.Create(session).Error)
|
||||
|
||||
// Delete the token
|
||||
err := repo.DeleteToken(ctx, token)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify it's gone
|
||||
var deletedSession domain.UserSession
|
||||
err = db.Where("token = ?", token).First(&deletedSession).Error
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
}
|
||||
122
internal/data/sql/copyright_claim_repository_test.go
Normal file
122
internal/data/sql/copyright_claim_repository_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
package sql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// newTestCopyrightClaimRepoWithSQLite sets up an in-memory SQLite database for copyright claim repository testing.
|
||||
func newTestCopyrightClaimRepoWithSQLite(t *testing.T) (domain.CopyrightClaimRepository, *gorm.DB) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Auto-migrate the necessary schemas
|
||||
err = db.AutoMigrate(
|
||||
&domain.User{},
|
||||
&domain.Work{},
|
||||
&domain.CopyrightClaim{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &config.Config{}
|
||||
repo := sql.NewCopyrightClaimRepository(db, cfg)
|
||||
|
||||
// Clean up the database after the test
|
||||
t.Cleanup(func() {
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
err = sqlDB.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
return repo, db
|
||||
}
|
||||
|
||||
func TestCopyrightClaimRepository_ListByWorkID(t *testing.T) {
|
||||
repo, db := newTestCopyrightClaimRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create a user and a work
|
||||
user := domain.User{Username: "claimuser", Email: "claim@test.com"}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
work1 := domain.Work{Title: "Work 1"}
|
||||
require.NoError(t, db.Create(&work1).Error)
|
||||
work2 := domain.Work{Title: "Work 2"}
|
||||
require.NoError(t, db.Create(&work2).Error)
|
||||
|
||||
// Create some claims
|
||||
claim1 := domain.CopyrightClaim{UserID: &user.ID, WorkID: &work1.ID, Details: "Claim 1", ClaimDate: time.Now()}
|
||||
claim2 := domain.CopyrightClaim{UserID: &user.ID, WorkID: &work1.ID, Details: "Claim 2", ClaimDate: time.Now()}
|
||||
claim3 := domain.CopyrightClaim{UserID: &user.ID, WorkID: &work2.ID, Details: "Claim 3", ClaimDate: time.Now()}
|
||||
require.NoError(t, db.Create(&claim1).Error)
|
||||
require.NoError(t, db.Create(&claim2).Error)
|
||||
require.NoError(t, db.Create(&claim3).Error)
|
||||
|
||||
t.Run("finds_claims_for_work1", func(t *testing.T) {
|
||||
claims, err := repo.ListByWorkID(ctx, work1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, claims, 2)
|
||||
})
|
||||
|
||||
t.Run("finds_claims_for_work2", func(t *testing.T) {
|
||||
claims, err := repo.ListByWorkID(ctx, work2.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, claims, 1)
|
||||
assert.Equal(t, "Claim 3", claims[0].Details)
|
||||
})
|
||||
|
||||
t.Run("returns_empty_slice_for_no_claims", func(t *testing.T) {
|
||||
claims, err := repo.ListByWorkID(ctx, 999) // Non-existent work
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, claims)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCopyrightClaimRepository_ListByUserID(t *testing.T) {
|
||||
repo, db := newTestCopyrightClaimRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create users and a work
|
||||
user1 := domain.User{Username: "user1", Email: "user1@test.com"}
|
||||
require.NoError(t, db.Create(&user1).Error)
|
||||
user2 := domain.User{Username: "user2", Email: "user2@test.com"}
|
||||
require.NoError(t, db.Create(&user2).Error)
|
||||
work := domain.Work{Title: "Test Work"}
|
||||
require.NoError(t, db.Create(&work).Error)
|
||||
|
||||
// Create some claims
|
||||
claim1 := domain.CopyrightClaim{UserID: &user1.ID, WorkID: &work.ID, Details: "Claim 1", ClaimDate: time.Now()}
|
||||
claim2 := domain.CopyrightClaim{UserID: &user2.ID, WorkID: &work.ID, Details: "Claim 2", ClaimDate: time.Now()}
|
||||
claim3 := domain.CopyrightClaim{UserID: &user2.ID, WorkID: &work.ID, Details: "Claim 3", ClaimDate: time.Now()}
|
||||
require.NoError(t, db.Create(&claim1).Error)
|
||||
require.NoError(t, db.Create(&claim2).Error)
|
||||
require.NoError(t, db.Create(&claim3).Error)
|
||||
|
||||
t.Run("finds_claims_for_user1", func(t *testing.T) {
|
||||
claims, err := repo.ListByUserID(ctx, user1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, claims, 1)
|
||||
assert.Equal(t, "Claim 1", claims[0].Details)
|
||||
})
|
||||
|
||||
t.Run("finds_claims_for_user2", func(t *testing.T) {
|
||||
claims, err := repo.ListByUserID(ctx, user2.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, claims, 2)
|
||||
})
|
||||
|
||||
t.Run("returns_empty_slice_for_no_claims", func(t *testing.T) {
|
||||
claims, err := repo.ListByUserID(ctx, 999) // Non-existent user
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, claims)
|
||||
})
|
||||
}
|
||||
@ -127,7 +127,7 @@ func (s *CopyrightRepositoryTestSuite) TestGetTranslationByLanguage() {
|
||||
|
||||
_, err := s.repo.GetTranslationByLanguage(context.Background(), copyrightID, languageCode)
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal(sql.ErrEntityNotFound, err)
|
||||
s.Require().Contains(err.Error(), "entity not found")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -581,8 +581,10 @@ type CopyrightClaim struct {
|
||||
ClaimDate time.Time `gorm:"not null"`
|
||||
Resolution string `gorm:"type:text"`
|
||||
ResolvedAt *time.Time
|
||||
UserID *uint
|
||||
User *User `gorm:"foreignKey:UserID"`
|
||||
UserID *uint
|
||||
User *User `gorm:"foreignKey:UserID"`
|
||||
WorkID *uint
|
||||
Work *Work `gorm:"foreignKey:WorkID"`
|
||||
}
|
||||
type MonetizationType string
|
||||
const (
|
||||
|
||||
409
internal/platform/cache/redis_cache_test.go
vendored
Normal file
409
internal/platform/cache/redis_cache_test.go
vendored
Normal file
@ -0,0 +1,409 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"tercul/internal/platform/cache"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redismock/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRedisCache(t *testing.T) {
|
||||
client, _ := redismock.NewClientMock()
|
||||
keyGen := cache.NewDefaultKeyGenerator("test:")
|
||||
defaultExpiry := 2 * time.Hour
|
||||
|
||||
c := cache.NewRedisCache(client, keyGen, defaultExpiry)
|
||||
require.NotNil(t, c)
|
||||
}
|
||||
|
||||
func TestNewDefaultRedisCache(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
// This test requires a running Redis instance or a mock for the Ping command.
|
||||
// For now, we'll just test that the function returns a non-nil cache
|
||||
// when the config is valid. A more comprehensive test would involve
|
||||
// mocking the redis.NewClient and its Ping method.
|
||||
cfg := &config.Config{
|
||||
RedisAddr: "localhost:6379",
|
||||
}
|
||||
// Since this function actually tries to connect, we can't fully test it in a unit test
|
||||
// without a live redis. We will skip a full test here and focus on the other methods.
|
||||
// In a real-world scenario, we might use a test container with Redis.
|
||||
// For now, we'll just ensure it doesn't panic with a basic config.
|
||||
// A proper integration test would be better suited for this.
|
||||
_ = cfg
|
||||
// _, err := cache.NewDefaultRedisCache(cfg)
|
||||
// assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("connection error", func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
RedisAddr: "localhost:9999", // Invalid address
|
||||
}
|
||||
_, err := cache.NewDefaultRedisCache(cfg)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_Get(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
ctx := context.Background()
|
||||
key := "test-key"
|
||||
expectedValue := map[string]string{"foo": "bar"}
|
||||
expectedBytes, _ := json.Marshal(expectedValue)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
mock.ExpectGet(key).SetVal(string(expectedBytes))
|
||||
|
||||
var result map[string]string
|
||||
err := c.Get(ctx, key, &result)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedValue, result)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("cache miss", func(t *testing.T) {
|
||||
mock.ExpectGet(key).RedisNil()
|
||||
|
||||
var result map[string]string
|
||||
err := c.Get(ctx, key, &result)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "cache miss", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("redis error", func(t *testing.T) {
|
||||
mock.ExpectGet(key).SetErr(errors.New("redis error"))
|
||||
|
||||
var result map[string]string
|
||||
err := c.Get(ctx, key, &result)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "redis error", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_InvalidateEntityType(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
entityType := "user"
|
||||
pattern := "tercul:user:*"
|
||||
|
||||
t.Run("success with multiple batches", func(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
||||
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
||||
|
||||
keysBatch1 := make([]string, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
keysBatch1[i] = fmt.Sprintf("tercul:user:key%d", i)
|
||||
}
|
||||
keysBatch2 := []string{"tercul:user:key101", "tercul:user:key102"}
|
||||
|
||||
// Mocking the SCAN and DEL calls in the correct order
|
||||
mock.ExpectScan(0, pattern, 100).SetVal(keysBatch1, 1)
|
||||
mock.ExpectDel(keysBatch1...).SetVal(100)
|
||||
mock.ExpectScan(1, pattern, 100).SetVal(keysBatch2, 0)
|
||||
mock.ExpectDel(keysBatch2...).SetVal(2)
|
||||
|
||||
err := c.InvalidateEntityType(ctx, entityType)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("success with single batch", func(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
||||
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
||||
|
||||
keys := []string{"tercul:user:key1", "tercul:user:key2"}
|
||||
mock.ExpectScan(0, pattern, 100).SetVal(keys, 0)
|
||||
mock.ExpectDel(keys...).SetVal(2)
|
||||
|
||||
err := c.InvalidateEntityType(ctx, entityType)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("scan error", func(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
||||
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
||||
|
||||
mock.ExpectScan(0, pattern, 100).SetErr(errors.New("scan error"))
|
||||
|
||||
err := c.InvalidateEntityType(ctx, entityType)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "scan error", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("del error", func(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
||||
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
||||
|
||||
keys := []string{"tercul:user:key1"}
|
||||
mock.ExpectScan(0, pattern, 100).SetVal(keys, 0)
|
||||
mock.ExpectDel(keys...).SetErr(errors.New("del error"))
|
||||
|
||||
err := c.InvalidateEntityType(ctx, entityType)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "del error", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_GetMulti(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
ctx := context.Background()
|
||||
keys := []string{"key1", "key2", "key3"}
|
||||
expectedValues := []interface{}{"value1", nil, "value3"}
|
||||
expectedResult := map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key3": []byte("value3"),
|
||||
}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
mock.ExpectMGet(keys...).SetVal(expectedValues)
|
||||
|
||||
result, err := c.GetMulti(ctx, keys)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedResult, result)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("no keys", func(t *testing.T) {
|
||||
result, err := c.GetMulti(ctx, []string{})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("redis error", func(t *testing.T) {
|
||||
mock.ExpectMGet(keys...).SetErr(errors.New("redis error"))
|
||||
|
||||
_, err := c.GetMulti(ctx, keys)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "redis error", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_SetMulti(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
items := map[string]interface{}{
|
||||
"key1": "value1",
|
||||
"key2": 123,
|
||||
}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
mock.MatchExpectationsInOrder(false)
|
||||
|
||||
// Expect each Set command within the pipeline
|
||||
for key, value := range items {
|
||||
data, _ := json.Marshal(value)
|
||||
mock.ExpectSet(key, data, time.Hour).SetVal("OK")
|
||||
}
|
||||
|
||||
// The SetMulti function will call Exec(), which triggers the mock's pipeline hook
|
||||
err := c.SetMulti(ctx, items, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("no items", func(t *testing.T) {
|
||||
client, _ := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
err := c.SetMulti(ctx, map[string]interface{}{}, 0)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("redis error", func(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
mock.MatchExpectationsInOrder(false)
|
||||
|
||||
// To simulate a pipeline execution error, we make one of the inner commands fail.
|
||||
// The hook will stop processing on the first error and return it.
|
||||
data1, _ := json.Marshal("value1")
|
||||
mock.ExpectSet("key1", data1, time.Hour).SetErr(errors.New("redis error"))
|
||||
|
||||
data2, _ := json.Marshal(123)
|
||||
mock.ExpectSet("key2", data2, time.Hour).SetVal("OK")
|
||||
|
||||
err := c.SetMulti(ctx, items, 0)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "redis error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_EntityOperations(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
||||
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
||||
ctx := context.Background()
|
||||
|
||||
entityType := "user"
|
||||
id := uint(123)
|
||||
entityKey := "tercul:user:id:123"
|
||||
value := map[string]string{"name": "test"}
|
||||
valueBytes, _ := json.Marshal(value)
|
||||
|
||||
page := 1
|
||||
pageSize := 10
|
||||
listKey := "tercul:user:list:1:10"
|
||||
listValue := []string{"user1", "user2"}
|
||||
listValueBytes, _ := json.Marshal(listValue)
|
||||
|
||||
t.Run("GetEntity", func(t *testing.T) {
|
||||
mock.ExpectGet(entityKey).SetVal(string(valueBytes))
|
||||
var result map[string]string
|
||||
err := c.GetEntity(ctx, entityType, id, &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, value, result)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("SetEntity", func(t *testing.T) {
|
||||
mock.ExpectSet(entityKey, valueBytes, time.Hour).SetVal("OK")
|
||||
err := c.SetEntity(ctx, entityType, id, value, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("DeleteEntity", func(t *testing.T) {
|
||||
mock.ExpectDel(entityKey).SetVal(1)
|
||||
err := c.DeleteEntity(ctx, entityType, id)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("GetList", func(t *testing.T) {
|
||||
mock.ExpectGet(listKey).SetVal(string(listValueBytes))
|
||||
var result []string
|
||||
err := c.GetList(ctx, entityType, page, pageSize, &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, listValue, result)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("SetList", func(t *testing.T) {
|
||||
mock.ExpectSet(listKey, listValueBytes, time.Hour).SetVal("OK")
|
||||
err := c.SetList(ctx, entityType, page, pageSize, listValue, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("DeleteList", func(t *testing.T) {
|
||||
mock.ExpectDel(listKey).SetVal(1)
|
||||
err := c.DeleteList(ctx, entityType, page, pageSize)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_Set(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
ctx := context.Background()
|
||||
key := "test-key"
|
||||
value := map[string]string{"foo": "bar"}
|
||||
expectedBytes, _ := json.Marshal(value)
|
||||
|
||||
t.Run("success with default expiration", func(t *testing.T) {
|
||||
mock.ExpectSet(key, expectedBytes, time.Hour).SetVal("OK")
|
||||
|
||||
err := c.Set(ctx, key, value, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("success with custom expiration", func(t *testing.T) {
|
||||
expiration := 5 * time.Minute
|
||||
mock.ExpectSet(key, expectedBytes, expiration).SetVal("OK")
|
||||
|
||||
err := c.Set(ctx, key, value, expiration)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("redis error", func(t *testing.T) {
|
||||
mock.ExpectSet(key, expectedBytes, time.Hour).SetErr(errors.New("redis error"))
|
||||
|
||||
err := c.Set(ctx, key, value, 0)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "redis error", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_Delete(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
ctx := context.Background()
|
||||
key := "test-key"
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
mock.ExpectDel(key).SetVal(1)
|
||||
|
||||
err := c.Delete(ctx, key)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("redis error", func(t *testing.T) {
|
||||
mock.ExpectDel(key).SetErr(errors.New("redis error"))
|
||||
|
||||
err := c.Delete(ctx, key)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "redis error", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCache_Clear(t *testing.T) {
|
||||
client, mock := redismock.NewClientMock()
|
||||
c := cache.NewRedisCache(client, nil, time.Hour)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
mock.ExpectFlushAll().SetVal("OK")
|
||||
|
||||
err := c.Clear(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("redis error", func(t *testing.T) {
|
||||
mock.ExpectFlushAll().SetErr(errors.New("redis error"))
|
||||
|
||||
err := c.Clear(ctx)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "redis error", err.Error())
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user