mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 02:51:34 +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-openapi/validate v0.24.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // 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/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 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
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 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
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=
|
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
|
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error
|
||||||
UpdateTrending(ctx context.Context) error
|
UpdateTrending(ctx context.Context) error
|
||||||
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, 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 {
|
type service struct {
|
||||||
@ -314,6 +315,12 @@ func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit
|
|||||||
return s.repo.GetTrendingWorks(ctx, timePeriod, 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 {
|
func (s *service) UpdateTrending(ctx context.Context) error {
|
||||||
ctx, span := s.tracer.Start(ctx, "UpdateTrending")
|
ctx, span := s.tracer.Start(ctx, "UpdateTrending")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|||||||
@ -305,14 +305,18 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the original ID to delete later, as the sourceStats.ID might be overwritten.
|
||||||
|
originalSourceStatsID := sourceStats.ID
|
||||||
|
|
||||||
var targetStats domain.WorkStats
|
var targetStats domain.WorkStats
|
||||||
err = tx.Where("work_id = ?", targetWorkID).First(&targetStats).Error
|
err = tx.Where("work_id = ?", targetWorkID).First(&targetStats).Error
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// If target has no stats, create new ones based on source stats.
|
// If target has no stats, create a new stats record for it.
|
||||||
sourceStats.ID = 0 // Let GORM create a new record
|
newStats := sourceStats
|
||||||
sourceStats.WorkID = targetWorkID
|
newStats.ID = 0
|
||||||
if err = tx.Create(&sourceStats).Error; err != nil {
|
newStats.WorkID = targetWorkID
|
||||||
|
if err = tx.Create(&newStats).Error; err != nil {
|
||||||
return fmt.Errorf("failed to create new target stats: %w", err)
|
return fmt.Errorf("failed to create new target stats: %w", err)
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@ -325,8 +329,8 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the old source stats
|
// Delete the old source stats using the stored original ID.
|
||||||
if err = tx.Delete(&domain.WorkStats{}, sourceStats.ID).Error; err != nil {
|
if err = tx.Delete(&domain.WorkStats{}, originalSourceStatsID).Error; err != nil {
|
||||||
return fmt.Errorf("failed to delete source work stats: %w", err)
|
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() {
|
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 := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
work.ID = 1
|
work.ID = 1
|
||||||
|
|
||||||
@ -111,17 +111,40 @@ func (s *WorkCommandsSuite) TestUpdateWork_EmptyLanguage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *WorkCommandsSuite) TestUpdateWork_RepoError() {
|
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 := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
work.ID = 1
|
work.ID = 1
|
||||||
s.repo.updateFunc = func(ctx context.Context, w *domain.Work) error {
|
s.repo.updateFunc = func(ctx context.Context, w *domain.Work) error {
|
||||||
return errors.New("db error")
|
return errors.New("db error")
|
||||||
}
|
}
|
||||||
err := s.commands.UpdateWork(context.Background(), work)
|
err := s.commands.UpdateWork(ctx, work)
|
||||||
assert.Error(s.T(), err)
|
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() {
|
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 := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
work.ID = 1
|
work.ID = 1
|
||||||
|
|
||||||
@ -142,13 +165,27 @@ func (s *WorkCommandsSuite) TestDeleteWork_ZeroID() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *WorkCommandsSuite) TestDeleteWork_RepoError() {
|
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 {
|
s.repo.deleteFunc = func(ctx context.Context, id uint) error {
|
||||||
return errors.New("db error")
|
return errors.New("db error")
|
||||||
}
|
}
|
||||||
err := s.commands.DeleteWork(context.Background(), 1)
|
err := s.commands.DeleteWork(ctx, 1)
|
||||||
assert.Error(s.T(), err)
|
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() {
|
func (s *WorkCommandsSuite) TestAnalyzeWork_Success() {
|
||||||
work := &domain.Work{
|
work := &domain.Work{
|
||||||
TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
|
TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
|
||||||
@ -221,82 +258,157 @@ func TestMergeWork_Integration(t *testing.T) {
|
|||||||
analyticsSvc := &mockAnalyticsService{}
|
analyticsSvc := &mockAnalyticsService{}
|
||||||
commands := NewWorkCommands(workRepo, searchClient, authzSvc, analyticsSvc)
|
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 ---
|
// --- Seed Data ---
|
||||||
author1 := &domain.Author{Name: "Author One"}
|
t.Run("Success", func(t *testing.T) {
|
||||||
db.Create(author1)
|
author1 := &domain.Author{Name: "Author One"}
|
||||||
author2 := &domain.Author{Name: "Author Two"}
|
db.Create(author1)
|
||||||
db.Create(author2)
|
author2 := &domain.Author{Name: "Author Two"}
|
||||||
|
db.Create(author2)
|
||||||
|
|
||||||
tag1 := &domain.Tag{Name: "Tag One"}
|
tag1 := &domain.Tag{Name: "Tag One"}
|
||||||
db.Create(tag1)
|
db.Create(tag1)
|
||||||
tag2 := &domain.Tag{Name: "Tag Two"}
|
tag2 := &domain.Tag{Name: "Tag Two"}
|
||||||
db.Create(tag2)
|
db.Create(tag2)
|
||||||
|
|
||||||
sourceWork := &domain.Work{
|
sourceWork := &domain.Work{
|
||||||
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
||||||
Title: "Source Work",
|
Title: "Source Work",
|
||||||
Authors: []*domain.Author{author1},
|
Authors: []*domain.Author{author1},
|
||||||
Tags: []*domain.Tag{tag1},
|
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")
|
|
||||||
}
|
}
|
||||||
if tr.Language == "fr" {
|
db.Create(sourceWork)
|
||||||
foundFr = true
|
db.Create(&domain.Translation{Title: "Source English", Language: "en", TranslatableID: sourceWork.ID, TranslatableType: "works"})
|
||||||
assert.Equal(t, "Source French", tr.Title, "Should merge source's French translation")
|
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)
|
||||||
assert.True(t, foundEn, "English translation should be present")
|
db.Create(&domain.Translation{Title: "Target English", Language: "en", TranslatableID: targetWork.ID, TranslatableType: "works"})
|
||||||
assert.True(t, foundFr, "French translation should be present")
|
db.Create(&domain.WorkStats{WorkID: targetWork.ID, Views: 20, Likes: 10})
|
||||||
|
|
||||||
assert.Len(t, finalTargetWork.Authors, 2, "Authors should be merged")
|
// --- Execute Merge ---
|
||||||
assert.Len(t, finalTargetWork.Tags, 2, "Tags should be merged")
|
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
|
// --- Assertions ---
|
||||||
var finalStats domain.WorkStats
|
// 1. Source work should be deleted
|
||||||
db.Where("work_id = ?", targetWork.ID).First(&finalStats)
|
var deletedWork domain.Work
|
||||||
assert.Equal(t, int64(30), finalStats.Views, "Views should be summed")
|
err = db.First(&deletedWork, sourceWork.ID).Error
|
||||||
assert.Equal(t, int64(15), finalStats.Likes, "Likes should be summed")
|
assert.Error(t, err)
|
||||||
|
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||||
|
|
||||||
// 4. Source stats should be deleted
|
// 2. Target work should have merged data
|
||||||
var deletedStats domain.WorkStats
|
var finalTargetWork domain.Work
|
||||||
err = db.First(&deletedStats, "work_id = ?", sourceWork.ID).Error
|
db.Preload("Translations").Preload("Authors").Preload("Tags").First(&finalTargetWork, targetWork.ID)
|
||||||
assert.Error(t, err, "Source stats should be deleted")
|
|
||||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
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)
|
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)
|
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)
|
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) {
|
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
|
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) {
|
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||||
if m.getWithTranslationsFunc != nil {
|
if m.getWithTranslationsFunc != nil {
|
||||||
return m.getWithTranslationsFunc(ctx, id)
|
return m.getWithTranslationsFunc(ctx, id)
|
||||||
|
|||||||
@ -11,6 +11,15 @@ type mockAnalyticsService struct {
|
|||||||
updateWorkSentimentFunc func(ctx context.Context, workID uint) error
|
updateWorkSentimentFunc func(ctx context.Context, workID uint) error
|
||||||
updateTranslationReadingTimeFunc func(ctx context.Context, translationID uint) error
|
updateTranslationReadingTimeFunc func(ctx context.Context, translationID uint) error
|
||||||
updateTranslationSentimentFunc 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 {
|
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||||
@ -78,6 +87,9 @@ func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, t
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||||
|
if m.getOrCreateWorkStatsFunc != nil {
|
||||||
|
return m.getOrCreateWorkStatsFunc(ctx, workID)
|
||||||
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
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)
|
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() {
|
func (s *WorkQueriesSuite) TestListWorks_Success() {
|
||||||
domainWorks := &domain.PaginatedResult[domain.Work]{
|
domainWorks := &domain.PaginatedResult[domain.Work]{
|
||||||
Items: []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)
|
_, err := s.repo.GetTranslationByLanguage(context.Background(), copyrightID, languageCode)
|
||||||
s.Require().Error(err)
|
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"`
|
ClaimDate time.Time `gorm:"not null"`
|
||||||
Resolution string `gorm:"type:text"`
|
Resolution string `gorm:"type:text"`
|
||||||
ResolvedAt *time.Time
|
ResolvedAt *time.Time
|
||||||
UserID *uint
|
UserID *uint
|
||||||
User *User `gorm:"foreignKey:UserID"`
|
User *User `gorm:"foreignKey:UserID"`
|
||||||
|
WorkID *uint
|
||||||
|
Work *Work `gorm:"foreignKey:WorkID"`
|
||||||
}
|
}
|
||||||
type MonetizationType string
|
type MonetizationType string
|
||||||
const (
|
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