tercul-backend/internal/adapters/graphql/integration_test.go
google-labs-jules[bot] b03820de02 Refactor: Improve quality, testing, and core business logic.
This commit introduces a significant refactoring to improve the application's quality, test coverage, and production readiness, focusing on core localization and business logic features.

Key changes include:
- Consolidated the `CreateTranslation` and `UpdateTranslation` commands into a single, more robust `CreateOrUpdateTranslation` command. This uses a database-level `Upsert` for atomicity.
- Centralized authorization for translatable entities into a new `CanEditEntity` check within the application service layer.
- Fixed a critical bug in the `MergeWork` command that caused a UNIQUE constraint violation when merging works with conflicting translations. The logic now intelligently handles language conflicts.
- Implemented decrementing for "like" counts in the analytics service when a like is deleted, ensuring accurate statistics.
- Stabilized the test suite by switching to a file-based database for integration tests, fixing test data isolation issues, and adding a unique index to the `Translation` model to enforce data integrity.
- Refactored manual mocks to use the `testify/mock` library for better consistency and maintainability.
2025-10-05 09:41:40 +00:00

1197 lines
35 KiB
Go

package graphql_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
graph "tercul/internal/adapters/graphql"
"tercul/internal/app/auth"
"tercul/internal/app/author"
"tercul/internal/app/bookmark"
"tercul/internal/app/collection"
"tercul/internal/app/comment"
"tercul/internal/app/like"
"tercul/internal/app/translation"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/observability"
platform_auth "tercul/internal/platform/auth"
"tercul/internal/testutil"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/suite"
)
// GraphQLIntegrationSuite is a test suite for GraphQL integration tests
type GraphQLIntegrationSuite struct {
testutil.IntegrationTestSuite
server *httptest.Server
client *http.Client
}
func (s *GraphQLIntegrationSuite) CreateAuthenticatedUser(username, email string, role domain.UserRole) (*domain.User, string) {
// Password can be fixed for tests
password := "password123"
// Register user
registerInput := auth.RegisterInput{
Username: username,
Email: email,
Password: password,
}
authResponse, err := s.App.Auth.Commands.Register(context.Background(), registerInput)
s.Require().NoError(err)
s.Require().NotNil(authResponse)
// Update user role if necessary
user := authResponse.User
token := authResponse.Token
if user.Role != role {
// This part is tricky. There is no UpdateUserRole command.
// For a test, I can update the DB directly.
s.DB.Model(&domain.User{}).Where("id = ?", user.ID).Update("role", role)
user.Role = role
// Re-generate token with the new role
jwtManager := platform_auth.NewJWTManager()
newToken, err := jwtManager.GenerateToken(user)
s.Require().NoError(err)
token = newToken
}
return user, token
}
// SetupSuite sets up the test suite
func (s *GraphQLIntegrationSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
// Create GraphQL server with the test resolver
resolver := &graph.Resolver{App: s.App}
c := graph.Config{Resolvers: resolver}
c.Directives.Binding = graph.Binding // Register the binding directive
// Create the server with the custom error presenter
srv := handler.NewDefaultServer(graph.NewExecutableSchema(c))
srv.SetErrorPresenter(graph.NewErrorPresenter())
// Create JWT manager and middleware
jwtManager := platform_auth.NewJWTManager()
reg := prometheus.NewRegistry()
metrics := observability.NewMetrics(reg)
// Create a middleware chain
var chain http.Handler
chain = srv
chain = platform_auth.GraphQLAuthMiddleware(jwtManager)(chain)
chain = metrics.PrometheusMiddleware(chain)
chain = observability.TracingMiddleware(chain)
chain = observability.RequestIDMiddleware(chain)
s.server = httptest.NewServer(chain)
s.client = s.server.Client()
}
// TearDownSuite tears down the test suite
func (s *GraphQLIntegrationSuite) TearDownSuite() {
s.IntegrationTestSuite.TearDownSuite()
s.server.Close()
}
// SetupTest sets up each test
func (s *GraphQLIntegrationSuite) SetupTest() {
s.IntegrationTestSuite.SetupTest()
s.DB.Exec("DELETE FROM trendings")
}
type GetWorkResponse struct {
Work struct {
ID string `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Content string `json:"content"`
} `json:"work"`
}
// TestQueryWork tests the work query
func (s *GraphQLIntegrationSuite) TestQueryWork() {
// Create a test work with content
work := s.CreateTestWork("Test Work", "en", "Test content for work")
// Define the query
query := `
query GetWork($id: ID!) {
work(id: $id) {
id
name
language
content
}
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", work.ID),
}
// Execute the query
response, err := executeGraphQL[GetWorkResponse](s, query, variables, nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL query should not return errors")
// Verify the response
s.Equal("Test Work", response.Data.Work.Name, "Work name should match")
s.Equal("Test content for work", response.Data.Work.Content, "Work content should match")
s.Equal("en", response.Data.Work.Language, "Work language should match")
}
type GetWorksResponse struct {
Works []struct {
ID string `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Content string `json:"content"`
} `json:"works"`
}
// TestQueryWorks tests the works query
func (s *GraphQLIntegrationSuite) TestQueryWorks() {
// Create test works
s.CreateTestWork("Test Work 1", "en", "Test content for work 1")
s.CreateTestWork("Test Work 2", "en", "Test content for work 2")
s.CreateTestWork("Test Work 3", "fr", "Test content for work 3")
// Define the query
query := `
query GetWorks {
works {
id
name
language
content
}
}
`
// Execute the query
response, err := executeGraphQL[GetWorksResponse](s, query, nil, nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL query should not return errors")
// Verify the response
s.True(len(response.Data.Works) >= 3, "GraphQL response should contain at least 3 works")
// Verify each work
foundWork1 := false
foundWork2 := false
foundWork3 := false
for _, work := range response.Data.Works {
if work.Name == "Test Work 1" {
foundWork1 = true
s.Equal("en", work.Language, "Work 1 language should match")
} else if work.Name == "Test Work 2" {
foundWork2 = true
s.Equal("en", work.Language, "Work 2 language should match")
} else if work.Name == "Test Work 3" {
foundWork3 = true
s.Equal("fr", work.Language, "Work 3 language should match")
}
}
s.True(foundWork1, "GraphQL response should contain work 1")
s.True(foundWork2, "GraphQL response should contain work 2")
s.True(foundWork3, "GraphQL response should contain work 3")
}
type CreateWorkResponse struct {
CreateWork struct {
ID string `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Content string `json:"content"`
} `json:"createWork"`
}
// TestCreateWork tests the createWork mutation
func (s *GraphQLIntegrationSuite) TestCreateWork() {
// Define the mutation
mutation := `
mutation CreateWork($input: WorkInput!) {
createWork(input: $input) {
id
name
language
content
}
}
`
// Define the variables
variables := map[string]interface{}{
"input": map[string]interface{}{
"name": "New Test Work",
"language": "en",
"content": "New test content",
},
}
// Execute the mutation
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
response, err := executeGraphQL[CreateWorkResponse](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
// Verify the response
s.NotNil(response.Data.CreateWork.ID, "Work ID should not be nil")
s.Equal("New Test Work", response.Data.CreateWork.Name, "Work name should match")
s.Equal("en", response.Data.CreateWork.Language, "Work language should match")
s.Equal("New test content", response.Data.CreateWork.Content, "Work content should match")
// Verify that the work was created in the repository
workID, err := strconv.ParseUint(response.Data.CreateWork.ID, 10, 64)
s.Require().NoError(err)
createdWork, err := s.App.Work.Queries.GetWorkByID(context.Background(), uint(workID))
s.Require().NoError(err)
s.Require().NotNil(createdWork)
s.Equal("New Test Work", createdWork.Title)
s.Equal("en", createdWork.Language)
translations, err := s.App.Translation.Queries.TranslationsByWorkID(context.Background(), createdWork.ID)
s.Require().NoError(err)
s.Require().Len(translations, 1)
s.Equal("New test content", translations[0].Content)
}
// TestGraphQLIntegrationSuite runs the test suite
func (s *GraphQLIntegrationSuite) TestRegisterValidation() {
s.Run("should return error for invalid input", func() {
// Define the mutation
mutation := `
mutation Register($input: RegisterInput!) {
register(input: $input) {
token
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"input": map[string]interface{}{
"username": "a", // Too short
"email": "invalid-email",
"password": "short",
"firstName": "123",
"lastName": "456",
},
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestLoginValidation() {
s.Run("should return error for invalid input", func() {
// Define the mutation
mutation := `
mutation Login($input: LoginInput!) {
login(input: $input) {
token
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"input": map[string]interface{}{
"email": "invalid-email",
"password": "short",
},
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestCreateWorkValidation() {
s.Run("should return error for invalid input", func() {
// Define the mutation
mutation := `
mutation CreateWork($input: WorkInput!) {
createWork(input: $input) {
id
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"input": map[string]interface{}{
"name": "a", // Too short
"language": "en-US", // Not 2 chars
},
}
// Execute the mutation
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestUpdateWorkValidation() {
s.Run("should return error for invalid input", func() {
// Arrange
work := s.CreateTestWork("Test Work", "en", "Test content")
// Define the mutation
mutation := `
mutation UpdateWork($id: ID!, $input: WorkInput!) {
updateWork(id: $id, input: $input) {
id
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", work.ID),
"input": map[string]interface{}{
"name": "a", // Too short
"language": "en-US", // Not 2 chars
},
}
// Execute the mutation
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestCreateAuthorValidation() {
s.Run("should return error for invalid input", func() {
// Define the mutation
mutation := `
mutation CreateAuthor($input: AuthorInput!) {
createAuthor(input: $input) {
id
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"input": map[string]interface{}{
"name": "a", // Too short
"language": "en-US", // Not 2 chars
},
}
// Execute the mutation
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestUpdateAuthorValidation() {
s.Run("should return error for invalid input", func() {
// Arrange
createdAuthor, err := s.App.Author.Commands.CreateAuthor(context.Background(), author.CreateAuthorInput{Name: "Test Author"})
s.Require().NoError(err)
// Define the mutation
mutation := `
mutation UpdateAuthor($id: ID!, $input: AuthorInput!) {
updateAuthor(id: $id, input: $input) {
id
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdAuthor.ID),
"input": map[string]interface{}{
"name": "a", // Too short
"language": "en-US", // Not 2 chars
},
}
// Execute the mutation
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestCreateTranslationValidation() {
s.Run("should return error for invalid input", func() {
// Arrange
work := s.CreateTestWork("Test Work", "en", "Test content")
// Define the mutation
mutation := `
mutation CreateTranslation($input: TranslationInput!) {
createTranslation(input: $input) {
id
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"input": map[string]interface{}{
"name": "a", // Too short
"language": "en-US", // Not 2 chars
"workId": fmt.Sprintf("%d", work.ID),
},
}
// Execute the mutation
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestUpdateTranslationValidation() {
s.Run("should return error for invalid input", func() {
// Arrange
work := s.CreateTestWork("Test Work", "en", "Test content")
createdTranslation, err := s.App.Translation.Commands.CreateOrUpdateTranslation(s.AdminCtx, translation.CreateOrUpdateTranslationInput{
Title: "Test Translation",
Language: "en",
Content: "Test content",
TranslatableID: work.ID,
TranslatableType: "works",
})
s.Require().NoError(err)
// Define the mutation
mutation := `
mutation UpdateTranslation($id: ID!, $input: TranslationInput!) {
updateTranslation(id: $id, input: $input) {
id
}
}
`
// Define the variables with invalid input
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdTranslation.ID),
"input": map[string]interface{}{
"name": "a", // Too short
"language": "en-US", // Not 2 chars
"workId": fmt.Sprintf("%d", work.ID),
},
}
// Execute the mutation
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
s.Len(response.Errors, 1)
})
}
func (s *GraphQLIntegrationSuite) TestDeleteWork() {
s.Run("should delete a work", func() {
// Arrange
work := s.CreateTestWork("Test Work", "en", "Test content")
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
// Define the mutation
mutation := `
mutation DeleteWork($id: ID!) {
deleteWork(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", work.ID),
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
s.Require().NotNil(response.Data)
s.True(response.Data.(map[string]interface{})["deleteWork"].(bool))
// Verify that the work was actually deleted from the database
_, err = s.App.Work.Queries.GetWorkByID(context.Background(), work.ID)
s.Require().Error(err)
})
}
func (s *GraphQLIntegrationSuite) TestDeleteAuthor() {
s.Run("should delete an author", func() {
// Arrange
createdAuthor, err := s.App.Author.Commands.CreateAuthor(context.Background(), author.CreateAuthorInput{Name: "Test Author"})
s.Require().NoError(err)
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
// Define the mutation
mutation := `
mutation DeleteAuthor($id: ID!) {
deleteAuthor(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdAuthor.ID),
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
s.Require().NotNil(response.Data)
s.True(response.Data.(map[string]interface{})["deleteAuthor"].(bool))
// Verify that the author was actually deleted from the database
_, err = s.App.Author.Queries.Author(context.Background(), createdAuthor.ID)
s.Require().Error(err)
})
}
func (s *GraphQLIntegrationSuite) TestDeleteTranslation() {
s.Run("should delete a translation", func() {
// Arrange
work := s.CreateTestWork("Test Work", "en", "Test content")
createdTranslation, err := s.App.Translation.Commands.CreateOrUpdateTranslation(s.AdminCtx, translation.CreateOrUpdateTranslationInput{
Title: "Test Translation",
Language: "en",
Content: "Test content",
TranslatableID: work.ID,
TranslatableType: "works",
})
s.Require().NoError(err)
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
// Define the mutation
mutation := `
mutation DeleteTranslation($id: ID!) {
deleteTranslation(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdTranslation.ID),
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
s.Require().NotNil(response.Data)
s.True(response.Data.(map[string]interface{})["deleteTranslation"].(bool))
// Verify that the translation was actually deleted from the database
_, err = s.App.Translation.Queries.Translation(context.Background(), createdTranslation.ID)
s.Require().Error(err)
})
}
func TestGraphQLIntegrationSuite(t *testing.T) {
testutil.SkipIfShort(t)
suite.Run(t, new(GraphQLIntegrationSuite))
}
type CreateCollectionResponse struct {
CreateCollection struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
} `json:"createCollection"`
}
type UpdateCollectionResponse struct {
UpdateCollection struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
} `json:"updateCollection"`
}
type AddWorkToCollectionResponse struct {
AddWorkToCollection struct {
ID string `json:"id"`
} `json:"addWorkToCollection"`
}
type RemoveWorkFromCollectionResponse struct {
RemoveWorkFromCollection struct {
ID string `json:"id"`
} `json:"removeWorkFromCollection"`
}
func (s *GraphQLIntegrationSuite) TestCommentMutations() {
// Create users for testing authorization
commenter, commenterToken := s.CreateAuthenticatedUser("commenter", "commenter@test.com", domain.UserRoleReader)
otherUser, otherToken := s.CreateAuthenticatedUser("otheruser", "other@test.com", domain.UserRoleReader)
_ = otherUser
// Create a work to comment on
work := s.CreateTestWork("Commentable Work", "en", "Some content")
var commentID string
s.Run("should create a comment on a work", func() {
// Define the mutation
mutation := `
mutation CreateComment($input: CommentInput!) {
createComment(input: $input) {
id
text
}
}
`
// Define the variables
variables := map[string]interface{}{
"input": map[string]interface{}{
"text": "This is a test comment.",
"workId": fmt.Sprintf("%d", work.ID),
},
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &commenterToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
// Verify the response
commentData := response.Data.(map[string]interface{})["createComment"].(map[string]interface{})
s.NotNil(commentData["id"], "Comment ID should not be nil")
commentID = commentData["id"].(string)
s.Equal("This is a test comment.", commentData["text"])
})
s.Run("should update a comment", func() {
// Define the mutation
mutation := `
mutation UpdateComment($id: ID!, $input: CommentInput!) {
updateComment(id: $id, input: $input) {
id
text
}
}
`
// Define the variables
variables := map[string]interface{}{
"id": commentID,
"input": map[string]interface{}{
"text": "This is an updated comment.",
},
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &commenterToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
// Verify the response
commentData := response.Data.(map[string]interface{})["updateComment"].(map[string]interface{})
s.Equal("This is an updated comment.", commentData["text"])
})
s.Run("should not update a comment owned by another user", func() {
// Define the mutation
mutation := `
mutation UpdateComment($id: ID!, $input: CommentInput!) {
updateComment(id: $id, input: $input) {
id
}
}
`
// Define the variables
variables := map[string]interface{}{
"id": commentID,
"input": map[string]interface{}{
"text": "Attempted Takeover",
},
}
// Execute the mutation with the other user's token
response, err := executeGraphQL[any](s, mutation, variables, &otherToken)
s.Require().NoError(err)
s.Require().NotNil(response.Errors)
})
s.Run("should delete a comment", func() {
// Create a new comment to delete
createdComment, err := s.App.Comment.Commands.CreateComment(context.Background(), comment.CreateCommentInput{
Text: "to be deleted",
UserID: commenter.ID,
WorkID: &work.ID,
})
s.Require().NoError(err)
// Define the mutation
mutation := `
mutation DeleteComment($id: ID!) {
deleteComment(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdComment.ID),
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &commenterToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
s.True(response.Data.(map[string]interface{})["deleteComment"].(bool))
})
}
func (s *GraphQLIntegrationSuite) TestLikeMutations() {
// Create users for testing authorization
liker, likerToken := s.CreateAuthenticatedUser("liker", "liker@test.com", domain.UserRoleReader)
otherUser, otherToken := s.CreateAuthenticatedUser("otheruser", "other@test.com", domain.UserRoleReader)
_ = otherUser
// Create a work to like
work := s.CreateTestWork("Likeable Work", "en", "Some content")
var likeID string
s.Run("should create a like on a work", func() {
// Define the mutation
mutation := `
mutation CreateLike($input: LikeInput!) {
createLike(input: $input) {
id
}
}
`
// Define the variables
variables := map[string]interface{}{
"input": map[string]interface{}{
"workId": fmt.Sprintf("%d", work.ID),
},
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &likerToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
// Verify the response
likeData := response.Data.(map[string]interface{})["createLike"].(map[string]interface{})
s.NotNil(likeData["id"], "Like ID should not be nil")
likeID = likeData["id"].(string)
})
s.Run("should not delete a like owned by another user", func() {
// Create a like by the original user
createdLike, err := s.App.Like.Commands.CreateLike(context.Background(), like.CreateLikeInput{
UserID: liker.ID,
WorkID: &work.ID,
})
s.Require().NoError(err)
// Define the mutation
mutation := `
mutation DeleteLike($id: ID!) {
deleteLike(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdLike.ID),
}
// Execute the mutation with the other user's token
response, err := executeGraphQL[any](s, mutation, variables, &otherToken)
s.Require().NoError(err)
s.Require().NotNil(response.Errors)
})
s.Run("should delete a like", func() {
// Use the likeID from the create test
// Define the mutation
mutation := `
mutation DeleteLike($id: ID!) {
deleteLike(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": likeID,
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &likerToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
s.True(response.Data.(map[string]interface{})["deleteLike"].(bool))
})
}
func (s *GraphQLIntegrationSuite) TestBookmarkMutations() {
// Create users for testing authorization
bookmarker, bookmarkerToken := s.CreateAuthenticatedUser("bookmarker", "bookmarker@test.com", domain.UserRoleReader)
otherUser, otherToken := s.CreateAuthenticatedUser("otheruser", "other@test.com", domain.UserRoleReader)
_ = otherUser
// Create a work to bookmark
work := s.CreateTestWork("Bookmarkable Work", "en", "Some content")
s.Run("should create a bookmark on a work", func() {
// Define the mutation
mutation := `
mutation CreateBookmark($input: BookmarkInput!) {
createBookmark(input: $input) {
id
}
}
`
// Define the variables
variables := map[string]interface{}{
"input": map[string]interface{}{
"workId": fmt.Sprintf("%d", work.ID),
},
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &bookmarkerToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
// Verify the response
bookmarkData := response.Data.(map[string]interface{})["createBookmark"].(map[string]interface{})
s.NotNil(bookmarkData["id"], "Bookmark ID should not be nil")
// Cleanup
bookmarkID, err := strconv.ParseUint(bookmarkData["id"].(string), 10, 32)
s.Require().NoError(err)
s.App.Bookmark.Commands.DeleteBookmark(context.Background(), uint(bookmarkID))
})
s.Run("should not delete a bookmark owned by another user", func() {
// Create a bookmark by the original user
createdBookmark, err := s.App.Bookmark.Commands.CreateBookmark(context.Background(), bookmark.CreateBookmarkInput{
UserID: bookmarker.ID,
WorkID: work.ID,
Name: "A Bookmark",
})
s.Require().NoError(err)
s.T().Cleanup(func() { s.App.Bookmark.Commands.DeleteBookmark(context.Background(), createdBookmark.ID) })
// Define the mutation
mutation := `
mutation DeleteBookmark($id: ID!) {
deleteBookmark(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdBookmark.ID),
}
// Execute the mutation with the other user's token
response, err := executeGraphQL[any](s, mutation, variables, &otherToken)
s.Require().NoError(err)
s.Require().NotNil(response.Errors)
})
s.Run("should delete a bookmark", func() {
// Create a new bookmark to delete
createdBookmark, err := s.App.Bookmark.Commands.CreateBookmark(context.Background(), bookmark.CreateBookmarkInput{
UserID: bookmarker.ID,
WorkID: work.ID,
Name: "To Be Deleted",
})
s.Require().NoError(err)
// Define the mutation
mutation := `
mutation DeleteBookmark($id: ID!) {
deleteBookmark(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": fmt.Sprintf("%d", createdBookmark.ID),
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &bookmarkerToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
s.True(response.Data.(map[string]interface{})["deleteBookmark"].(bool))
})
}
type TrendingWorksResponse struct {
TrendingWorks []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"trendingWorks"`
}
func (s *GraphQLIntegrationSuite) TestTrendingWorksQuery() {
s.Run("should return a list of trending works", func() {
// Arrange
work1 := s.CreateTestWork("Work 1", "en", "content")
work2 := s.CreateTestWork("Work 2", "en", "content")
s.DB.Create(&work.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
s.DB.Create(&work.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
s.Require().NoError(s.App.Analytics.UpdateTrending(context.Background()))
// Act
query := `
query GetTrendingWorks {
trendingWorks {
id
name
}
}
`
response, err := executeGraphQL[TrendingWorksResponse](s, query, nil, nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL query should not return errors")
// Assert
s.Len(response.Data.TrendingWorks, 2)
s.Equal(fmt.Sprintf("%d", work2.ID), response.Data.TrendingWorks[0].ID)
})
}
func (s *GraphQLIntegrationSuite) TestCollectionMutations() {
// Create users for testing authorization
owner, ownerToken := s.CreateAuthenticatedUser("collectionowner", "owner@test.com", domain.UserRoleReader)
otherUser, otherToken := s.CreateAuthenticatedUser("otheruser", "other@test.com", domain.UserRoleReader)
_ = otherUser
var collectionID string
s.Run("should create a collection", func() {
// Define the mutation
mutation := `
mutation CreateCollection($input: CollectionInput!) {
createCollection(input: $input) {
id
name
description
}
}
`
// Define the variables
variables := map[string]interface{}{
"input": map[string]interface{}{
"name": "My New Collection",
"description": "A collection of my favorite works.",
},
}
// Execute the mutation
response, err := executeGraphQL[CreateCollectionResponse](s, mutation, variables, &ownerToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
// Verify the response
s.NotNil(response.Data.CreateCollection.ID, "Collection ID should not be nil")
collectionID = response.Data.CreateCollection.ID // Save for later tests
s.Equal("My New Collection", response.Data.CreateCollection.Name, "Collection name should match")
s.Equal("A collection of my favorite works.", response.Data.CreateCollection.Description, "Collection description should match")
})
s.Run("should update a collection", func() {
// Define the mutation
mutation := `
mutation UpdateCollection($id: ID!, $input: CollectionInput!) {
updateCollection(id: $id, input: $input) {
id
name
description
}
}
`
// Define the variables
variables := map[string]interface{}{
"id": collectionID,
"input": map[string]interface{}{
"name": "My Updated Collection",
"description": "An updated description.",
},
}
// Execute the mutation
response, err := executeGraphQL[UpdateCollectionResponse](s, mutation, variables, &ownerToken)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
// Verify the response
s.Equal("My Updated Collection", response.Data.UpdateCollection.Name)
})
s.Run("should not update a collection owned by another user", func() {
// Define the mutation
mutation := `
mutation UpdateCollection($id: ID!, $input: CollectionInput!) {
updateCollection(id: $id, input: $input) {
id
}
}
`
// Define the variables
variables := map[string]interface{}{
"id": collectionID,
"input": map[string]interface{}{
"name": "Attempted Takeover",
},
}
// Execute the mutation with the other user's token
response, err := executeGraphQL[any](s, mutation, variables, &otherToken)
s.Require().NoError(err)
s.Require().NotNil(response.Errors)
})
s.Run("should add a work to a collection", func() {
// Create a work
work := s.CreateTestWork("Test Work", "en", "Test content")
// Define the mutation
mutation := `
mutation AddWorkToCollection($collectionId: ID!, $workId: ID!) {
addWorkToCollection(collectionId: $collectionId, workId: $workId) {
id
}
}
`
// Define the variables
variables := map[string]interface{}{
"collectionId": collectionID,
"workId": fmt.Sprintf("%d", work.ID),
}
// Execute the mutation
response, err := executeGraphQL[AddWorkToCollectionResponse](s, mutation, variables, &ownerToken)
s.Require().NoError(err)
s.Require().Nil(response.Errors)
})
s.Run("should remove a work from a collection", func() {
// Create a work and add it to the collection first
work := s.CreateTestWork("Another Work", "en", "Some content")
collectionIDInt, err := strconv.ParseUint(collectionID, 10, 64)
s.Require().NoError(err)
err = s.App.Collection.Commands.AddWorkToCollection(context.Background(), collection.AddWorkToCollectionInput{
CollectionID: uint(collectionIDInt),
WorkID: work.ID,
UserID: owner.ID,
})
s.Require().NoError(err)
// Define the mutation
mutation := `
mutation RemoveWorkFromCollection($collectionId: ID!, $workId: ID!) {
removeWorkFromCollection(collectionId: $collectionId, workId: $workId) {
id
}
}
`
// Define the variables
variables := map[string]interface{}{
"collectionId": collectionID,
"workId": fmt.Sprintf("%d", work.ID),
}
// Execute the mutation
response, err := executeGraphQL[RemoveWorkFromCollectionResponse](s, mutation, variables, &ownerToken)
s.Require().NoError(err)
s.Require().Nil(response.Errors)
})
s.Run("should delete a collection", func() {
// Define the mutation
mutation := `
mutation DeleteCollection($id: ID!) {
deleteCollection(id: $id)
}
`
// Define the variables
variables := map[string]interface{}{
"id": collectionID,
}
// Execute the mutation
response, err := executeGraphQL[any](s, mutation, variables, &ownerToken)
s.Require().NoError(err)
s.Require().Nil(response.Errors)
s.True(response.Data.(map[string]interface{})["deleteCollection"].(bool))
})
}