mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 00:31:35 +00:00
This commit includes a major refactoring of the GORM many-to-many relationships to use explicit join tables, improving stability and compatibility with GORM's features. It also implements a large number of previously unimplemented GraphQL mutations for core entities like Collections, Comments, Likes, and Bookmarks. Key changes: - Refactored polymorphic many-to-many relationships for Copyright and Monetization to use standard many-to-many with explicit join tables. - Implemented GraphQL mutations for Collection, Comment, Like, and Bookmark entities, including input validation and authorization checks. - Added comprehensive integration tests for all new features and refactored code. - Refactored the GraphQL integration test suite to be type-safe, using generics for response handling as requested. - Updated repository interfaces and implementations to support the new data model. - Updated the TODO.md file to reflect the completed work.
1133 lines
32 KiB
Go
1133 lines
32 KiB
Go
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()
|
|
}
|
|
|
|
// 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
|
|
// Since we're using the real repository interface, we can query it
|
|
works, err := s.WorkRepo.ListAll(context.Background())
|
|
s.Require().NoError(err)
|
|
|
|
var found bool
|
|
for _, w := range works {
|
|
if w.Title == "New Test Work" {
|
|
found = true
|
|
s.Equal("en", w.Language, "Work language should be set correctly")
|
|
break
|
|
}
|
|
}
|
|
s.True(found, "Work should be created in repository")
|
|
}
|
|
|
|
// 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 := &domain.Author{Name: "Test Author"}
|
|
s.Require().NoError(s.AuthorRepo.Create(context.Background(), author))
|
|
|
|
// 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 := &domain.Translation{
|
|
Title: "Test Translation",
|
|
Language: "en",
|
|
Content: "Test content",
|
|
TranslatableID: work.ID,
|
|
TranslatableType: "Work",
|
|
}
|
|
s.Require().NoError(s.TranslationRepo.Create(context.Background(), translation))
|
|
|
|
// 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.WorkRepo.GetByID(context.Background(), work.ID)
|
|
s.Require().Error(err)
|
|
})
|
|
}
|
|
|
|
func (s *GraphQLIntegrationSuite) TestDeleteAuthor() {
|
|
s.Run("should delete an author", func() {
|
|
// Arrange
|
|
author := &domain.Author{Name: "Test Author"}
|
|
s.Require().NoError(s.AuthorRepo.Create(context.Background(), author))
|
|
|
|
// 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.AuthorRepo.GetByID(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 := &domain.Translation{
|
|
Title: "Test Translation",
|
|
Language: "en",
|
|
Content: "Test content",
|
|
TranslatableID: work.ID,
|
|
TranslatableType: "Work",
|
|
}
|
|
s.Require().NoError(s.TranslationRepo.Create(context.Background(), translation))
|
|
|
|
// 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.TranslationRepo.GetByID(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 := &domain.Comment{Text: "to be deleted", UserID: commenter.ID, WorkID: &work.ID}
|
|
s.Require().NoError(s.App.CommentRepo.Create(context.Background(), comment))
|
|
|
|
// 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 := &domain.Like{UserID: liker.ID, WorkID: &work.ID}
|
|
s.Require().NoError(s.App.LikeRepo.Create(context.Background(), like))
|
|
|
|
// 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.BookmarkRepo.Delete(context.Background(), uint(bookmarkID))
|
|
})
|
|
|
|
s.Run("should not delete a bookmark owned by another user", func() {
|
|
// Create a bookmark by the original user
|
|
bookmark := &domain.Bookmark{UserID: bookmarker.ID, WorkID: work.ID, Name: "A Bookmark"}
|
|
s.Require().NoError(s.App.BookmarkRepo.Create(context.Background(), bookmark))
|
|
s.T().Cleanup(func() { s.App.BookmarkRepo.Delete(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 := &domain.Bookmark{UserID: bookmarker.ID, WorkID: work.ID, Name: "To Be Deleted"}
|
|
s.Require().NoError(s.App.BookmarkRepo.Create(context.Background(), bookmark))
|
|
|
|
// 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))
|
|
})
|
|
}
|
|
|
|
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")
|
|
s.Require().NoError(s.App.CollectionRepo.AddWorkToCollection(context.Background(), owner.ID, work.ID))
|
|
|
|
// 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))
|
|
})
|
|
}
|