Skip to content

Commit 0d925f7

Browse files
committed
infernal books tests and mock mismatch hell gorm is terrible
1 parent 5b66a32 commit 0d925f7

File tree

6 files changed

+232
-17
lines changed

6 files changed

+232
-17
lines changed

cmd/server/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
func main() {
4141
redisClient := cache.NewRedisClient()
4242
db := database.NewDatabase()
43+
dbWrapper := &database.GormDatabase{DB: db}
4344
mongo := database.SetupMongoDB()
4445
ctx := context.Background()
4546
logger, _ := zap.NewProduction()
@@ -48,7 +49,7 @@ func main() {
4849
//gin.SetMode(gin.ReleaseMode)
4950
gin.SetMode(gin.DebugMode)
5051

51-
r := api.NewRouter(logger, mongo, db, redisClient, &ctx)
52+
r := api.NewRouter(logger, mongo, dbWrapper, redisClient, &ctx)
5253

5354
if err := r.Run(":8001"); err != nil {
5455
log.Fatal(err)

pkg/api/books.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func (r *bookRepository) CreateBook(c *gin.Context) {
171171
func (r *bookRepository) FindBook(c *gin.Context) {
172172
var book models.Book
173173

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

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

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

237-
appCtx.DB.Delete(&book)
232+
r.DB.Delete(&book)
238233

239234
c.JSON(http.StatusNoContent, gin.H{"data": true})
240235
}

pkg/api/books_test.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package api
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"golang-rest-api-template/pkg/cache"
@@ -95,3 +96,175 @@ func TestFindBooks(t *testing.T) {
9596
assert.Equal(t, http.StatusOK, w.Code)
9697
assert.Contains(t, w.Body.String(), "Book One")
9798
}
99+
100+
func TestCreateBook(t *testing.T) {
101+
ctrl := gomock.NewController(t)
102+
defer ctrl.Finish()
103+
104+
mockDB := database.NewMockDatabase(ctrl)
105+
mockCache := cache.NewMockCache(ctrl)
106+
ctx := context.Background()
107+
108+
repo := NewBookRepository(mockDB, mockCache, &ctx)
109+
110+
// Set up Gin
111+
gin.SetMode(gin.TestMode)
112+
r := gin.Default()
113+
r.POST("/books", func(c *gin.Context) {
114+
// Set the appCtx in the Gin context
115+
c.Set("appCtx", repo)
116+
repo.CreateBook(c)
117+
})
118+
119+
// Example data for the test
120+
inputBook := models.CreateBook{Title: "New Book", Author: "New Author"}
121+
requestBody, err := json.Marshal(inputBook)
122+
if err != nil {
123+
t.Fatalf("Failed to marshal input book data: %v", err)
124+
}
125+
126+
// Set up database mock to simulate successful book creation
127+
mockDB.EXPECT().Create(gomock.Any()).DoAndReturn(func(book *models.Book) *gorm.DB {
128+
// Normally, you might simulate setting an ID or other fields modified by the DB
129+
return &gorm.DB{Error: nil}
130+
})
131+
132+
// Set up cache mock to simulate key retrieval and deletion
133+
keyPattern := "books_offset_*"
134+
mockCache.EXPECT().Keys(ctx, keyPattern).Return(redis.NewStringSliceResult([]string{"books_offset_0_limit_10"}, nil))
135+
mockCache.EXPECT().Del(ctx, "books_offset_0_limit_10").Return(redis.NewIntResult(1, nil))
136+
137+
w := httptest.NewRecorder()
138+
req, err := http.NewRequest("POST", "/books", bytes.NewBuffer(requestBody))
139+
if err != nil {
140+
t.Fatalf("Failed to create the HTTP request: %v", err)
141+
}
142+
req.Header.Set("Content-Type", "application/json")
143+
144+
// Serve the HTTP request
145+
r.ServeHTTP(w, req)
146+
147+
// Assertions to check the response
148+
assert.Equal(t, http.StatusCreated, w.Code, "Expected HTTP status code 201")
149+
assert.Contains(t, w.Body.String(), "New Book", "Response body should contain the book title")
150+
}
151+
152+
func TestFindBook(t *testing.T) {
153+
ctrl := gomock.NewController(t)
154+
defer ctrl.Finish()
155+
156+
mockDB := database.NewMockDatabase(ctrl)
157+
ctx := context.Background()
158+
repo := NewBookRepository(mockDB, nil, &ctx)
159+
160+
// Set up Gin
161+
gin.SetMode(gin.TestMode)
162+
r := gin.Default()
163+
r.GET("/book/:id", repo.FindBook)
164+
165+
// Prepare test data
166+
expectedBook := models.Book{
167+
ID: 1,
168+
Title: "Effective Go",
169+
Author: "Robert Griesemer",
170+
}
171+
172+
// Mock expectations
173+
174+
// Mock the Where method
175+
mockDB.EXPECT().
176+
Where("id = ?", "1").
177+
DoAndReturn(func(query interface{}, args ...interface{}) database.Database {
178+
// Return mockDB to allow method chaining
179+
return mockDB
180+
}).Times(1)
181+
182+
// Mock the First method
183+
mockDB.EXPECT().
184+
First(gomock.Any()).
185+
DoAndReturn(func(dest interface{}, conds ...interface{}) database.Database {
186+
if b, ok := dest.(*models.Book); ok {
187+
*b = expectedBook
188+
}
189+
return mockDB
190+
}).Times(1)
191+
192+
// Mock the Error method or field access
193+
mockDB.EXPECT().
194+
Error().
195+
Return(nil).
196+
Times(1)
197+
198+
// Perform the request
199+
w := httptest.NewRecorder()
200+
req := httptest.NewRequest(http.MethodGet, "/book/1", nil)
201+
r.ServeHTTP(w, req)
202+
203+
// Assert response
204+
assert.Equal(t, http.StatusOK, w.Code)
205+
206+
var response struct {
207+
Status int `json:"status"`
208+
Message string `json:"message"`
209+
Data models.Book `json:"data"`
210+
}
211+
212+
err := json.NewDecoder(w.Body).Decode(&response)
213+
assert.NoError(t, err)
214+
assert.Equal(t, expectedBook.ID, response.Data.ID)
215+
assert.Equal(t, expectedBook.Title, response.Data.Title)
216+
assert.Equal(t, expectedBook.Author, response.Data.Author)
217+
}
218+
219+
func TestDeleteBook(t *testing.T) {
220+
ctrl := gomock.NewController(t)
221+
defer ctrl.Finish()
222+
223+
// Create mock for the database
224+
mockDB := database.NewMockDatabase(ctrl)
225+
ctx := context.Background()
226+
repo := NewBookRepository(mockDB, nil, &ctx)
227+
228+
// Set up Gin for testing
229+
gin.SetMode(gin.TestMode)
230+
r := gin.Default()
231+
r.DELETE("/book/:id", repo.DeleteBook)
232+
233+
// Prepare the book data
234+
existingBook := models.Book{
235+
ID: 1,
236+
Title: "Test Book",
237+
Author: "Test Author",
238+
}
239+
240+
// Mock Where to return the existingBook for chaining
241+
mockDB.EXPECT().
242+
Where("id = ?", "1").
243+
Return(mockDB).Times(1)
244+
245+
// Mock First to load the existingBook and return mockDB
246+
mockDB.EXPECT().
247+
First(gomock.Any()).
248+
DoAndReturn(func(dest interface{}, conds ...interface{}) database.Database {
249+
if b, ok := dest.(*models.Book); ok {
250+
*b = existingBook
251+
}
252+
return mockDB
253+
}).Times(1)
254+
255+
// Mock Delete method
256+
mockDB.EXPECT().
257+
Delete(&existingBook).
258+
Return(&gorm.DB{Error: nil}).Times(1)
259+
260+
// Mock Error method to return nil
261+
mockDB.EXPECT().Error().Return(nil).AnyTimes()
262+
263+
// Perform the DELETE request
264+
w := httptest.NewRecorder()
265+
req, _ := http.NewRequest(http.MethodDelete, "/book/1", nil)
266+
r.ServeHTTP(w, req)
267+
268+
// Assert the response
269+
assert.Equal(t, http.StatusNoContent, w.Code)
270+
}

pkg/api/user.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (r *userRepository) LoginHandler(c *gin.Context) {
6060
}
6161

6262
// Fetch the user from the database
63-
if err := r.DB.Where("username = ?", incomingUser.Username).First(&dbUser).Error; err != nil {
63+
if err := r.DB.Where("username = ?", incomingUser.Username).First(&dbUser).Error(); err != nil {
6464
if errors.Is(err, gorm.ErrRecordNotFound) {
6565
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
6666
} else {

pkg/database/db.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,29 @@ type Database interface {
1717
Limit(limit int) *gorm.DB
1818
Find(interface{}, ...interface{}) *gorm.DB
1919
Create(value interface{}) *gorm.DB
20-
Where(query interface{}, args ...interface{}) *gorm.DB
20+
Where(query interface{}, args ...interface{}) Database
2121
Delete(interface{}, ...interface{}) *gorm.DB
2222
Model(model interface{}) *gorm.DB
23-
First(dest interface{}, conds ...interface{}) *gorm.DB
23+
First(dest interface{}, conds ...interface{}) Database
2424
Updates(interface{}) *gorm.DB
25+
Order(value interface{}) *gorm.DB
26+
Error() error
27+
}
28+
29+
type GormDatabase struct {
30+
*gorm.DB
31+
}
32+
33+
func (db *GormDatabase) Where(query interface{}, args ...interface{}) Database {
34+
return &GormDatabase{db.DB.Where(query, args...)}
35+
}
36+
37+
func (db *GormDatabase) First(dest interface{}, conds ...interface{}) Database {
38+
return &GormDatabase{db.DB.First(dest, conds...)}
39+
}
40+
41+
func (db *GormDatabase) Error() error {
42+
return db.DB.Error
2543
}
2644

2745
func NewDatabase() *gorm.DB {

pkg/database/db_mock.go

Lines changed: 32 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)