tercul-backend/internal/data/sql/book_repository_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

163 lines
5.0 KiB
Go

package sql_test
import (
"context"
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
)
type BookRepositoryTestSuite struct {
testutil.IntegrationTestSuite
BookRepo domain.BookRepository
}
func (s *BookRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.BookRepo = sql.NewBookRepository(s.DB, cfg)
}
func (s *BookRepositoryTestSuite) SetupTest() {
s.IntegrationTestSuite.SetupTest()
s.DB.Exec("DELETE FROM books")
s.DB.Exec("DELETE FROM authors")
s.DB.Exec("DELETE FROM publishers")
s.DB.Exec("DELETE FROM book_authors")
s.DB.Exec("DELETE FROM book_works")
s.DB.Exec("DELETE FROM works")
}
func (s *BookRepositoryTestSuite) createBook(title, isbn string) *domain.Book {
book := &domain.Book{
Title: title,
ISBN: isbn,
TranslatableModel: domain.TranslatableModel{
Language: "en",
},
}
err := s.BookRepo.Create(context.Background(), book)
s.Require().NoError(err)
return book
}
func (s *BookRepositoryTestSuite) createAuthor(name string) *domain.Author {
author := &domain.Author{Name: name}
err := s.DB.Create(author).Error
s.Require().NoError(err)
return author
}
func (s *BookRepositoryTestSuite) createPublisher(name string) *domain.Publisher {
publisher := &domain.Publisher{Name: name}
err := s.DB.Create(publisher).Error
s.Require().NoError(err)
return publisher
}
func (s *BookRepositoryTestSuite) TestFindByISBN() {
s.Run("should return a book by ISBN", func() {
// Arrange
s.createBook("Test Book", "1234567890")
// Act
foundBook, err := s.BookRepo.FindByISBN(context.Background(), "1234567890")
// Assert
s.Require().NoError(err)
s.Require().NotNil(foundBook)
s.Equal("Test Book", foundBook.Title)
})
s.Run("should return error if ISBN not found", func() {
// Arrange
s.createBook("Another Book", "1111111111")
// Act
_, err := s.BookRepo.FindByISBN(context.Background(), "9999999999")
// Assert
s.Require().Error(err)
})
}
func TestBookRepository(t *testing.T) {
suite.Run(t, new(BookRepositoryTestSuite))
}
func (s *BookRepositoryTestSuite) TestListByAuthorID() {
s.Run("should return all books for a given author", func() {
// Arrange
author1 := s.createAuthor("Test Author 1")
author2 := s.createAuthor("Test Author 2")
book1 := s.createBook("Book 1 by Author 1", "111")
book2 := s.createBook("Book 2 by Author 1", "222")
book3 := s.createBook("Book 3 by Author 2", "333")
s.Require().NoError(s.DB.Model(&author1).Association("Books").Append([]*domain.Book{book1, book2}))
s.Require().NoError(s.DB.Model(&author2).Association("Books").Append(book3))
// Act
books, err := s.BookRepo.ListByAuthorID(context.Background(), author1.ID)
// Assert
s.Require().NoError(err)
s.Len(books, 2)
s.ElementsMatch([]string{"Book 1 by Author 1", "Book 2 by Author 1"}, []string{books[0].Title, books[1].Title})
})
}
func (s *BookRepositoryTestSuite) TestListByPublisherID() {
s.Run("should return all books for a given publisher", func() {
// Arrange
publisher1 := s.createPublisher("Publisher 1")
publisher2 := s.createPublisher("Publisher 2")
book1 := s.createBook("Book 1 from Publisher 1", "111")
book2 := s.createBook("Book 2 from Publisher 1", "222")
book3 := s.createBook("Book 3 from Publisher 2", "333")
book1.PublisherID = &publisher1.ID
book2.PublisherID = &publisher1.ID
book3.PublisherID = &publisher2.ID
s.Require().NoError(s.DB.Save(book1).Error)
s.Require().NoError(s.DB.Save(book2).Error)
s.Require().NoError(s.DB.Save(book3).Error)
// Act
books, err := s.BookRepo.ListByPublisherID(context.Background(), publisher1.ID)
// Assert
s.Require().NoError(err)
s.Len(books, 2)
s.ElementsMatch([]string{"Book 1 from Publisher 1", "Book 2 from Publisher 1"}, []string{books[0].Title, books[1].Title})
})
}
func (s *BookRepositoryTestSuite) TestListByWorkID() {
s.Run("should return all books associated with a given work", func() {
// Arrange
work1 := s.CreateTestWork(s.AdminCtx, "Work 1", "en", "content 1")
work2 := s.CreateTestWork(s.AdminCtx, "Work 2", "en", "content 2")
book1 := s.createBook("Book 1 for Work 1", "111")
book2 := s.createBook("Book 2 for Work 1", "222")
book3 := s.createBook("Book 3 for Work 2", "333")
// Manually create the association in the join table
s.Require().NoError(s.DB.Exec("INSERT INTO book_works (book_id, work_id) VALUES (?, ?)", book1.ID, work1.ID).Error)
s.Require().NoError(s.DB.Exec("INSERT INTO book_works (book_id, work_id) VALUES (?, ?)", book2.ID, work1.ID).Error)
s.Require().NoError(s.DB.Exec("INSERT INTO book_works (book_id, work_id) VALUES (?, ?)", book3.ID, work2.ID).Error)
// Act
books, err := s.BookRepo.ListByWorkID(context.Background(), work1.ID)
// Assert
s.Require().NoError(err)
s.Len(books, 2)
s.ElementsMatch([]string{"Book 1 for Work 1", "Book 2 for Work 1"}, []string{books[0].Title, books[1].Title})
})
}