package graphql_test import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strconv" "testing" graph "tercul/internal/adapters/graphql" "tercul/internal/domain" platform_auth "tercul/internal/platform/auth" "tercul/internal/testutil" "github.com/99designs/gqlgen/graphql/handler" "github.com/stretchr/testify/suite" ) // GraphQLRequest represents a GraphQL request type GraphQLRequest struct { Query string `json:"query"` OperationName string `json:"operationName,omitempty"` Variables map[string]interface{} `json:"variables,omitempty"` } // GraphQLResponse represents a generic GraphQL response type GraphQLResponse[T any] struct { Data T `json:"data,omitempty"` Errors []map[string]interface{} `json:"errors,omitempty"` } // GraphQLIntegrationSuite is a test suite for GraphQL integration tests type GraphQLIntegrationSuite struct { testutil.IntegrationTestSuite server *httptest.Server client *http.Client } // SetupSuite sets up the test suite func (s *GraphQLIntegrationSuite) SetupSuite() { s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) // Create GraphQL server with the test resolver resolver := s.GetResolver() srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: resolver})) // Create JWT manager and middleware jwtManager := platform_auth.NewJWTManager() authMiddleware := platform_auth.GraphQLAuthMiddleware(jwtManager) s.server = httptest.NewServer(authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv.ServeHTTP(w, r) }))) 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") } // executeGraphQL executes a GraphQL query and decodes the response into a generic type func executeGraphQL[T any](s *GraphQLIntegrationSuite, query string, variables map[string]interface{}, token *string) (*GraphQLResponse[T], error) { // Create the request request := GraphQLRequest{ Query: query, Variables: variables, } // Marshal the request to JSON requestBody, err := json.Marshal(request) if err != nil { return nil, err } // Create an HTTP request req, err := http.NewRequest("POST", s.server.URL, bytes.NewBuffer(requestBody)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if token != nil { req.Header.Set("Authorization", "Bearer "+*token) } // Execute the request resp, err := s.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Parse the response var response GraphQLResponse[T] err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return nil, err } return &response, nil } 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 response, err := executeGraphQL[CreateWorkResponse](s, mutation, variables, nil) 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.WorkQueries.Work(context.Background(), uint(workID)) s.Require().NoError(err) s.Require().NotNil(createdWork) s.Equal("New Test Work", createdWork.Title) s.Equal("en", createdWork.Language) s.Equal("New test content", createdWork.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 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) 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 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) 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 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) TestUpdateAuthorValidation() { s.Run("should return error for invalid input", func() { // Arrange author, 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", author.ID), "input": map[string]interface{}{ "name": "a", // Too short "language": "en-US", // Not 2 chars }, } // 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) 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 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) TestUpdateTranslationValidation() { s.Run("should return error for invalid input", func() { // Arrange work := s.CreateTestWork("Test Work", "en", "Test content") translation, err := s.App.Translation.Commands.CreateTranslation(context.Background(), translation.CreateTranslationInput{ Title: "Test Translation", Language: "en", Content: "Test content", TranslatableID: work.ID, TranslatableType: "Work", }) 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", translation.ID), "input": map[string]interface{}{ "name": "a", // Too short "language": "en-US", // Not 2 chars "workId": fmt.Sprintf("%d", work.ID), }, } // 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) TestDeleteWork() { s.Run("should delete a work", func() { // Arrange work := s.CreateTestWork("Test Work", "en", "Test content") // 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, nil) 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.WorkQueries.Work(context.Background(), work.ID) s.Require().Error(err) }) } func (s *GraphQLIntegrationSuite) TestDeleteAuthor() { s.Run("should delete an author", func() { // Arrange author, err := s.App.Author.Commands.CreateAuthor(context.Background(), author.CreateAuthorInput{Name: "Test Author"}) s.Require().NoError(err) // Define the mutation mutation := ` mutation DeleteAuthor($id: ID!) { deleteAuthor(id: $id) } ` // Define the variables variables := map[string]interface{}{ "id": fmt.Sprintf("%d", author.ID), } // Execute the mutation response, err := executeGraphQL[any](s, mutation, variables, nil) 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(), author.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") translation, err := s.App.Translation.Commands.CreateTranslation(context.Background(), translation.CreateTranslationInput{ Title: "Test Translation", Language: "en", Content: "Test content", TranslatableID: work.ID, TranslatableType: "Work", }) s.Require().NoError(err) // Define the mutation mutation := ` mutation DeleteTranslation($id: ID!) { deleteTranslation(id: $id) } ` // Define the variables variables := map[string]interface{}{ "id": fmt.Sprintf("%d", translation.ID), } // Execute the mutation response, err := executeGraphQL[any](s, mutation, variables, nil) 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(), translation.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 comment, 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", comment.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 like, 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", like.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 bookmark, 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(), bookmark.ID) }) // Define the mutation mutation := ` mutation DeleteBookmark($id: ID!) { deleteBookmark(id: $id) } ` // Define the variables variables := map[string]interface{}{ "id": fmt.Sprintf("%d", bookmark.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 bookmark, 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", bookmark.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(&domain.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1}) s.DB.Create(&domain.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10}) s.Require().NoError(s.App.AnalyticsService.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, }) 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)) }) }