Skip to content

Commit

Permalink
infernal books tests and mock mismatch hell gorm is terrible
Browse files Browse the repository at this point in the history
  • Loading branch information
araujo88 committed Oct 22, 2024
1 parent 5b66a32 commit 0d925f7
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 17 deletions.
3 changes: 2 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
func main() {
redisClient := cache.NewRedisClient()
db := database.NewDatabase()
dbWrapper := &database.GormDatabase{DB: db}
mongo := database.SetupMongoDB()
ctx := context.Background()
logger, _ := zap.NewProduction()
Expand All @@ -48,7 +49,7 @@ func main() {
//gin.SetMode(gin.ReleaseMode)
gin.SetMode(gin.DebugMode)

r := api.NewRouter(logger, mongo, db, redisClient, &ctx)
r := api.NewRouter(logger, mongo, dbWrapper, redisClient, &ctx)

if err := r.Run(":8001"); err != nil {
log.Fatal(err)
Expand Down
13 changes: 4 additions & 9 deletions pkg/api/books.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (r *bookRepository) CreateBook(c *gin.Context) {
func (r *bookRepository) FindBook(c *gin.Context) {
var book models.Book

if err := r.DB.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
if err := r.DB.Where("id = ?", c.Param("id")).First(&book).Error(); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "book not found"})
return
}
Expand All @@ -196,7 +196,7 @@ func (r *bookRepository) UpdateBook(c *gin.Context) {
var book models.Book
var input models.UpdateBook

if err := r.DB.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
if err := r.DB.Where("id = ?", c.Param("id")).First(&book).Error(); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "book not found"})
return
}
Expand All @@ -222,19 +222,14 @@ func (r *bookRepository) UpdateBook(c *gin.Context) {
// @Failure 404 {string} string "book not found"
// @Router /books/{id} [delete]
func (r *bookRepository) DeleteBook(c *gin.Context) {
appCtx, exists := c.MustGet("appCtx").(*bookRepository)
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
var book models.Book

if err := appCtx.DB.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
if err := r.DB.Where("id = ?", c.Param("id")).First(&book).Error(); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "book not found"})
return
}

appCtx.DB.Delete(&book)
r.DB.Delete(&book)

c.JSON(http.StatusNoContent, gin.H{"data": true})
}
173 changes: 173 additions & 0 deletions pkg/api/books_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"bytes"
"context"
"encoding/json"
"golang-rest-api-template/pkg/cache"
Expand Down Expand Up @@ -95,3 +96,175 @@ func TestFindBooks(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "Book One")
}

func TestCreateBook(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockDB := database.NewMockDatabase(ctrl)
mockCache := cache.NewMockCache(ctrl)
ctx := context.Background()

repo := NewBookRepository(mockDB, mockCache, &ctx)

// Set up Gin
gin.SetMode(gin.TestMode)
r := gin.Default()
r.POST("/books", func(c *gin.Context) {
// Set the appCtx in the Gin context
c.Set("appCtx", repo)
repo.CreateBook(c)
})

// Example data for the test
inputBook := models.CreateBook{Title: "New Book", Author: "New Author"}
requestBody, err := json.Marshal(inputBook)
if err != nil {
t.Fatalf("Failed to marshal input book data: %v", err)
}

// Set up database mock to simulate successful book creation
mockDB.EXPECT().Create(gomock.Any()).DoAndReturn(func(book *models.Book) *gorm.DB {
// Normally, you might simulate setting an ID or other fields modified by the DB
return &gorm.DB{Error: nil}
})

// Set up cache mock to simulate key retrieval and deletion
keyPattern := "books_offset_*"
mockCache.EXPECT().Keys(ctx, keyPattern).Return(redis.NewStringSliceResult([]string{"books_offset_0_limit_10"}, nil))
mockCache.EXPECT().Del(ctx, "books_offset_0_limit_10").Return(redis.NewIntResult(1, nil))

w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/books", bytes.NewBuffer(requestBody))
if err != nil {
t.Fatalf("Failed to create the HTTP request: %v", err)
}
req.Header.Set("Content-Type", "application/json")

// Serve the HTTP request
r.ServeHTTP(w, req)

// Assertions to check the response
assert.Equal(t, http.StatusCreated, w.Code, "Expected HTTP status code 201")
assert.Contains(t, w.Body.String(), "New Book", "Response body should contain the book title")
}

func TestFindBook(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockDB := database.NewMockDatabase(ctrl)
ctx := context.Background()
repo := NewBookRepository(mockDB, nil, &ctx)

// Set up Gin
gin.SetMode(gin.TestMode)
r := gin.Default()
r.GET("/book/:id", repo.FindBook)

// Prepare test data
expectedBook := models.Book{
ID: 1,
Title: "Effective Go",
Author: "Robert Griesemer",
}

// Mock expectations

// Mock the Where method
mockDB.EXPECT().
Where("id = ?", "1").
DoAndReturn(func(query interface{}, args ...interface{}) database.Database {
// Return mockDB to allow method chaining
return mockDB
}).Times(1)

// Mock the First method
mockDB.EXPECT().
First(gomock.Any()).
DoAndReturn(func(dest interface{}, conds ...interface{}) database.Database {
if b, ok := dest.(*models.Book); ok {
*b = expectedBook
}
return mockDB
}).Times(1)

// Mock the Error method or field access
mockDB.EXPECT().
Error().
Return(nil).
Times(1)

// Perform the request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/book/1", nil)
r.ServeHTTP(w, req)

// Assert response
assert.Equal(t, http.StatusOK, w.Code)

var response struct {
Status int `json:"status"`
Message string `json:"message"`
Data models.Book `json:"data"`
}

err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
assert.Equal(t, expectedBook.ID, response.Data.ID)
assert.Equal(t, expectedBook.Title, response.Data.Title)
assert.Equal(t, expectedBook.Author, response.Data.Author)
}

func TestDeleteBook(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Create mock for the database
mockDB := database.NewMockDatabase(ctrl)
ctx := context.Background()
repo := NewBookRepository(mockDB, nil, &ctx)

// Set up Gin for testing
gin.SetMode(gin.TestMode)
r := gin.Default()
r.DELETE("/book/:id", repo.DeleteBook)

// Prepare the book data
existingBook := models.Book{
ID: 1,
Title: "Test Book",
Author: "Test Author",
}

// Mock Where to return the existingBook for chaining
mockDB.EXPECT().
Where("id = ?", "1").
Return(mockDB).Times(1)

// Mock First to load the existingBook and return mockDB
mockDB.EXPECT().
First(gomock.Any()).
DoAndReturn(func(dest interface{}, conds ...interface{}) database.Database {
if b, ok := dest.(*models.Book); ok {
*b = existingBook
}
return mockDB
}).Times(1)

// Mock Delete method
mockDB.EXPECT().
Delete(&existingBook).
Return(&gorm.DB{Error: nil}).Times(1)

// Mock Error method to return nil
mockDB.EXPECT().Error().Return(nil).AnyTimes()

// Perform the DELETE request
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodDelete, "/book/1", nil)
r.ServeHTTP(w, req)

// Assert the response
assert.Equal(t, http.StatusNoContent, w.Code)
}
2 changes: 1 addition & 1 deletion pkg/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (r *userRepository) LoginHandler(c *gin.Context) {
}

// Fetch the user from the database
if err := r.DB.Where("username = ?", incomingUser.Username).First(&dbUser).Error; err != nil {
if err := r.DB.Where("username = ?", incomingUser.Username).First(&dbUser).Error(); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
} else {
Expand Down
22 changes: 20 additions & 2 deletions pkg/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,29 @@ type Database interface {
Limit(limit int) *gorm.DB
Find(interface{}, ...interface{}) *gorm.DB
Create(value interface{}) *gorm.DB
Where(query interface{}, args ...interface{}) *gorm.DB
Where(query interface{}, args ...interface{}) Database
Delete(interface{}, ...interface{}) *gorm.DB
Model(model interface{}) *gorm.DB
First(dest interface{}, conds ...interface{}) *gorm.DB
First(dest interface{}, conds ...interface{}) Database
Updates(interface{}) *gorm.DB
Order(value interface{}) *gorm.DB
Error() error
}

type GormDatabase struct {
*gorm.DB
}

func (db *GormDatabase) Where(query interface{}, args ...interface{}) Database {
return &GormDatabase{db.DB.Where(query, args...)}
}

func (db *GormDatabase) First(dest interface{}, conds ...interface{}) Database {
return &GormDatabase{db.DB.First(dest, conds...)}
}

func (db *GormDatabase) Error() error {
return db.DB.Error
}

func NewDatabase() *gorm.DB {
Expand Down
36 changes: 32 additions & 4 deletions pkg/database/db_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0d925f7

Please sign in to comment.