mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
This commit completes the Domain-Driven Design (DDD) refactoring, bringing the codebase into a stable, compilable, and fully tested state.
Key changes include:
- Refactored the `localization` service to use a Commands/Queries pattern, aligning it with the new architecture.
- Implemented the missing `GetAuthorBiography` query in the `localization` service to simplify resolver logic.
- Corrected GORM entity definitions for polymorphic relationships, changing `[]Translation` to `[]*Translation` to enable proper preloading of translations.
- Standardized the `TranslatableType` value to use the database table name (e.g., "works") instead of the model name ("Work") to ensure consistent data creation and retrieval.
- Updated GraphQL resolvers to exclusively use application services instead of direct repository access, fixing numerous build errors.
- Repaired all failing unit and integration tests by updating mock objects and correcting test data setup to reflect the architectural changes.
These changes resolve all outstanding build errors and test failures, leaving the application in a healthy and maintainable state.
1221 lines
35 KiB
Go
1221 lines
35 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/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"
|
|
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
|
|
}
|
|
|
|
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
|
|
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
|
|
}
|
|
|
|
return user, authResponse.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}
|
|
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.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
|
|
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
|
|
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
|
|
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")
|
|
createdTranslation, err := s.App.Translation.Commands.CreateTranslation(context.Background(), translation.CreateTranslationInput{
|
|
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
|
|
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.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)
|
|
|
|
// 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, 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(), 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.CreateTranslation(context.Background(), translation.CreateTranslationInput{
|
|
Title: "Test Translation",
|
|
Language: "en",
|
|
Content: "Test content",
|
|
TranslatableID: work.ID,
|
|
TranslatableType: "works",
|
|
})
|
|
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", createdTranslation.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(), 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(&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.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
|
|
_, 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))
|
|
})
|
|
} |