tercul-backend/internal/adapters/graphql/work_resolvers_test.go
google-labs-jules[bot] c2e9a118e2 feat(testing): Increase test coverage and fix authz bugs
This commit significantly increases the test coverage across the application and fixes several underlying bugs that were discovered while writing the new tests.

The key changes include:

- **New Tests:** Added extensive integration and unit tests for GraphQL resolvers, application services, and data repositories, substantially increasing the test coverage for packages like `graphql`, `user`, `translation`, and `analytics`.

- **Authorization Bug Fixes:**
  - Fixed a critical bug where a user creating a `Work` was not correctly associated as its author, causing subsequent permission failures.
  - Corrected the authorization logic in `authz.Service` to properly check for entity ownership by non-admin users.

- **Test Refactoring:**
  - Refactored numerous test suites to use `testify/mock` instead of manual mocks, improving test clarity and maintainability.
  - Isolated integration tests by creating a fresh admin user and token for each test run, eliminating test pollution.
  - Centralized domain errors into `internal/domain/errors.go` and updated repositories to use them, making error handling more consistent.

- **Code Quality Improvements:**
  - Replaced manual mock implementations with `testify/mock` for better consistency.
  - Cleaned up redundant and outdated test files.

These changes stabilize the test suite, improve the overall quality of the codebase, and move the project closer to the goal of 80% test coverage.
2025-10-09 07:03:45 +00:00

260 lines
7.5 KiB
Go

package graphql_test
import (
"context"
"fmt"
"os"
"testing"
"tercul/internal/adapters/graphql"
"tercul/internal/adapters/graphql/model"
"tercul/internal/app/auth"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
)
type WorkResolversTestSuite struct {
testutil.IntegrationTestSuite
queryResolver graphql.QueryResolver
mutationResolver graphql.MutationResolver
}
func TestWorkResolvers(t *testing.T) {
suite.Run(t, new(WorkResolversTestSuite))
}
func (s *WorkResolversTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(&testutil.TestConfig{
DBPath: "work_resolvers_test.db",
})
}
func (s *WorkResolversTestSuite) TearDownSuite() {
s.IntegrationTestSuite.TearDownSuite()
os.Remove("work_resolvers_test.db")
}
func (s *WorkResolversTestSuite) SetupTest() {
s.IntegrationTestSuite.SetupTest()
resolver := &graphql.Resolver{App: s.App}
s.queryResolver = resolver.Query()
s.mutationResolver = resolver.Mutation()
}
// Helper to create a user for tests
func (s *WorkResolversTestSuite) createUser(username, email, password string, role domain.UserRole) *domain.User {
resp, err := s.App.Auth.Commands.Register(context.Background(), auth.RegisterInput{
Username: username,
Email: email,
Password: password,
})
s.Require().NoError(err)
user, err := s.App.User.Queries.User(context.Background(), resp.User.ID)
s.Require().NoError(err)
if role != user.Role {
user.Role = role
err = s.DB.Save(user).Error
s.Require().NoError(err)
}
return user
}
// Helper to create a context with JWT claims
func (s *WorkResolversTestSuite) contextWithClaims(user *domain.User) context.Context {
return testutil.ContextWithClaims(context.Background(), &platform_auth.Claims{
UserID: user.ID,
Role: string(user.Role),
})
}
func (s *WorkResolversTestSuite) TestCreateWork() {
user := s.createUser("work-creator", "work-creator@test.com", "password", domain.UserRoleContributor)
ctx := s.contextWithClaims(user)
s.Run("Success", func() {
// Arrange
input := model.WorkInput{
Name: "My First Work",
Language: "en",
}
// Act
work, err := s.mutationResolver.CreateWork(ctx, input)
// Assert
s.Require().NoError(err)
s.Require().NotNil(work)
s.Equal("My First Work", work.Name)
s.Equal("en", work.Language)
// Verify in DB
dbWork, err := s.App.Work.Queries.GetWorkByID(context.Background(), 1)
s.Require().NoError(err)
s.Require().NotNil(dbWork)
s.Equal("My First Work", dbWork.Title)
})
}
func (s *WorkResolversTestSuite) TestWorkQuery() {
// Arrange
user := s.createUser("work-reader", "work-reader@test.com", "password", domain.UserRoleReader)
ctx := s.contextWithClaims(user)
// Create a work to query
domainWork := &domain.Work{Title: "Query Me", TranslatableModel: domain.TranslatableModel{Language: "es"}}
createdWork, err := s.App.Work.Commands.CreateWork(ctx, domainWork)
s.Require().NoError(err)
workID := fmt.Sprintf("%d", createdWork.ID)
s.Run("Success", func() {
// Act
work, err := s.queryResolver.Work(ctx, workID)
// Assert
s.Require().NoError(err)
s.Require().NotNil(work)
s.Equal("Query Me", work.Name)
s.Equal("es", work.Language)
})
s.Run("Not Found", func() {
// Act
work, err := s.queryResolver.Work(ctx, "99999")
// Assert
s.Require().NoError(err)
s.Require().Nil(work)
})
}
func (s *WorkResolversTestSuite) TestUpdateWork() {
// Arrange
user := s.createUser("work-updater", "work-updater@test.com", "password", domain.UserRoleContributor)
admin := s.createUser("work-admin", "work-admin@test.com", "password", domain.UserRoleAdmin)
otherUser := s.createUser("other-user", "other-user@test.com", "password", domain.UserRoleContributor)
// Create a work to update
domainWork := &domain.Work{Title: "Update Me", TranslatableModel: domain.TranslatableModel{Language: "fr"}}
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
s.Require().NoError(err)
workID := fmt.Sprintf("%d", createdWork.ID)
s.Run("Success as owner", func() {
// Arrange
ctx := s.contextWithClaims(user)
input := model.WorkInput{Name: "Updated Title", Language: "fr"}
// Act
work, err := s.mutationResolver.UpdateWork(ctx, workID, input)
// Assert
s.Require().NoError(err)
s.Equal("Updated Title", work.Name)
})
s.Run("Success as admin", func() {
// Arrange
ctx := s.contextWithClaims(admin)
input := model.WorkInput{Name: "Updated by Admin", Language: "fr"}
// Act
work, err := s.mutationResolver.UpdateWork(ctx, workID, input)
// Assert
s.Require().NoError(err)
s.Equal("Updated by Admin", work.Name)
})
s.Run("Forbidden for other user", func() {
// Arrange
ctx := s.contextWithClaims(otherUser)
input := model.WorkInput{Name: "Should Not Update", Language: "fr"}
// Act
_, err := s.mutationResolver.UpdateWork(ctx, workID, input)
// Assert
s.Require().Error(err)
s.ErrorIs(err, domain.ErrForbidden)
})
}
func (s *WorkResolversTestSuite) TestDeleteWork() {
// Arrange
user := s.createUser("work-deletor", "work-deletor@test.com", "password", domain.UserRoleContributor)
admin := s.createUser("work-admin-deletor", "work-admin-deletor@test.com", "password", domain.UserRoleAdmin)
otherUser := s.createUser("other-user-deletor", "other-user-deletor@test.com", "password", domain.UserRoleContributor)
s.Run("Success as owner", func() {
// Arrange
domainWork := &domain.Work{Title: "Delete Me", TranslatableModel: domain.TranslatableModel{Language: "de"}}
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
s.Require().NoError(err)
workID := fmt.Sprintf("%d", createdWork.ID)
ctx := s.contextWithClaims(user)
// Act
ok, err := s.mutationResolver.DeleteWork(ctx, workID)
// Assert
s.Require().NoError(err)
s.True(ok)
})
s.Run("Success as admin", func() {
// Arrange
domainWork := &domain.Work{Title: "Delete Me Admin", TranslatableModel: domain.TranslatableModel{Language: "de"}}
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
s.Require().NoError(err)
workID := fmt.Sprintf("%d", createdWork.ID)
ctx := s.contextWithClaims(admin)
// Act
ok, err := s.mutationResolver.DeleteWork(ctx, workID)
// Assert
s.Require().NoError(err)
s.True(ok)
})
s.Run("Forbidden for other user", func() {
// Arrange
domainWork := &domain.Work{Title: "Don't Delete Me", TranslatableModel: domain.TranslatableModel{Language: "de"}}
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
s.Require().NoError(err)
workID := fmt.Sprintf("%d", createdWork.ID)
ctx := s.contextWithClaims(otherUser)
// Act
_, err = s.mutationResolver.DeleteWork(ctx, workID)
// Assert
s.Require().Error(err)
s.ErrorIs(err, domain.ErrForbidden)
})
}
func (s *WorkResolversTestSuite) TestWorksQuery() {
// Arrange
user := s.createUser("works-reader", "works-reader@test.com", "password", domain.UserRoleReader)
ctx := s.contextWithClaims(user)
// Create some works
_, err := s.App.Work.Commands.CreateWork(ctx, &domain.Work{Title: "Work 1", TranslatableModel: domain.TranslatableModel{Language: "en"}})
s.Require().NoError(err)
_, err = s.App.Work.Commands.CreateWork(ctx, &domain.Work{Title: "Work 2", TranslatableModel: domain.TranslatableModel{Language: "en"}})
s.Require().NoError(err)
s.Run("Success", func() {
// Act
works, err := s.queryResolver.Works(ctx, nil, nil, nil, nil, nil, nil, nil)
// Assert
s.Require().NoError(err)
s.True(len(works) >= 2) // >= because other tests might have created works
})
}