@@ -89,6 +89,7 @@ func TestProductHandler_GetAllProducts(t *testing.T) {
8989 }
9090
9191 for _ , tc := range tests {
92+ tc := tc // Capture range variable
9293 t .Run (tc .name , func (t * testing.T ) {
9394 mockRepo .On ("FindAll" , mock .Anything ).Return (tc .mockReturn , tc .mockError ).Once ()
9495
@@ -152,6 +153,7 @@ func TestProductHandler_GetProduct(t *testing.T) {
152153 }
153154
154155 for _ , tc := range tests {
156+ tc := tc // Capture range variable
155157 t .Run (tc .name , func (t * testing.T ) {
156158 // Moved setup inside t.Run for isolation
157159 mockRepo , _ , _ , _ , router := setupProductTest (t )
@@ -266,6 +268,7 @@ func TestProductHandler_CreateProduct(t *testing.T) {
266268 }
267269
268270 for _ , tc := range tests {
271+ tc := tc // Capture range variable
269272 t .Run (tc .name , func (t * testing.T ) {
270273 // Moved setup inside t.Run for isolation
271274 mockProductRepo , mockUserRepo , _ , _ , router := setupProductTest (t )
@@ -306,5 +309,262 @@ func TestProductHandler_CreateProduct(t *testing.T) {
306309 }
307310}
308311
309- // TODO: Add tests for UpdateProduct
310- // TODO: Add tests for DeleteProduct
312+ func TestProductHandler_UpdateProduct (t * testing.T ) {
313+ // Setup inside loop
314+ testUserID := uuid .New ()
315+ productToUpdateID := uuid .New ()
316+ testJwtSecret := "test-secret-for-jwt-please-change"
317+ testToken := generateTestToken (testUserID , testJwtSecret )
318+
319+ tests := []struct {
320+ name string
321+ productID string
322+ body string
323+ mockUserReturn * models.User
324+ mockUserErr error
325+ mockUpdateErr error
326+ expectedStatus int
327+ expectedBody string
328+ }{
329+ {
330+ name : "Success" ,
331+ productID : productToUpdateID .String (),
332+ body : `{"name":"Updated Gadget","description":"Better","price":129.99}` , // Include all fields
333+ mockUserReturn : & models.User {ID : testUserID },
334+ mockUserErr : nil ,
335+ mockUpdateErr : nil ,
336+ expectedStatus : http .StatusOK ,
337+ expectedBody : "Updated Gadget" ,
338+ },
339+ {
340+ name : "Failure - Invalid UUID" ,
341+ productID : "not-a-uuid" ,
342+ body : `{"name":"Update Attempt"}` ,
343+ mockUserReturn : & models.User {ID : testUserID },
344+ mockUserErr : nil ,
345+ mockUpdateErr : nil ,
346+ expectedStatus : http .StatusNotFound , // Expect 404 from router
347+ expectedBody : "404 page not found" ,
348+ },
349+ {
350+ name : "Failure - Invalid JSON" ,
351+ productID : productToUpdateID .String (),
352+ body : `{"name":}` , // Invalid JSON
353+ mockUserReturn : & models.User {ID : testUserID },
354+ mockUserErr : nil ,
355+ mockUpdateErr : nil ,
356+ expectedStatus : http .StatusBadRequest ,
357+ expectedBody : `{"error":"invalid request body"}` ,
358+ },
359+ {
360+ name : "Failure - Missing Name" ,
361+ productID : productToUpdateID .String (),
362+ body : `{"price":50.0}` , // Missing name
363+ mockUserReturn : & models.User {ID : testUserID },
364+ mockUserErr : nil ,
365+ mockUpdateErr : nil ,
366+ expectedStatus : http .StatusBadRequest ,
367+ expectedBody : `{"error":"product name is required and price must be non-negative"}` ,
368+ },
369+ {
370+ name : "Failure - Negative Price" ,
371+ productID : productToUpdateID .String (),
372+ body : `{"name":"Bad Price Product","price":-1.0}` ,
373+ mockUserReturn : & models.User {ID : testUserID },
374+ mockUserErr : nil ,
375+ mockUpdateErr : nil ,
376+ expectedStatus : http .StatusBadRequest ,
377+ expectedBody : `{"error":"product name is required and price must be non-negative"}` ,
378+ },
379+ {
380+ name : "Failure - Product Not Found" ,
381+ productID : productToUpdateID .String (),
382+ body : `{"name":"Update Attempt","price":10.0}` ,
383+ mockUserReturn : & models.User {ID : testUserID },
384+ mockUserErr : nil ,
385+ mockUpdateErr : products .ErrProductNotFound ,
386+ expectedStatus : http .StatusNotFound ,
387+ expectedBody : `{"error":"product not found"}` ,
388+ },
389+ {
390+ name : "Failure - Repo Update Error" ,
391+ productID : productToUpdateID .String (),
392+ body : `{"name":"Update Attempt","price":10.0}` ,
393+ mockUserReturn : & models.User {ID : testUserID },
394+ mockUserErr : nil ,
395+ mockUpdateErr : errors .New ("db update failed" ),
396+ expectedStatus : http .StatusInternalServerError ,
397+ expectedBody : `{"error":"failed to update product"}` ,
398+ },
399+ {
400+ name : "Failure - Middleware User Check Fails" ,
401+ productID : productToUpdateID .String (),
402+ body : `{"name":"Update Attempt","price":10.0}` ,
403+ mockUserReturn : nil ,
404+ mockUserErr : users .ErrUserNotFound ,
405+ mockUpdateErr : nil ,
406+ expectedStatus : http .StatusUnauthorized ,
407+ expectedBody : `{"error":"user associated with token not found"}` ,
408+ },
409+ {
410+ name : "Failure - No Auth Token" ,
411+ productID : productToUpdateID .String (),
412+ body : `{"name":"Update Attempt","price":10.0}` ,
413+ mockUserReturn : nil ,
414+ mockUserErr : nil ,
415+ mockUpdateErr : nil ,
416+ expectedStatus : http .StatusUnauthorized ,
417+ expectedBody : `{"error":"authorization header required"}` ,
418+ },
419+ }
420+
421+ for _ , tc := range tests {
422+ tc := tc // Capture range variable
423+ t .Run (tc .name , func (t * testing.T ) {
424+ // Moved setup inside t.Run for isolation
425+ mockProductRepo , mockUserRepo , _ , _ , router := setupProductTest (t )
426+
427+ // Mock middleware user check
428+ if tc .expectedStatus != http .StatusUnauthorized || tc .expectedBody == `{"error":"user associated with token not found"}` {
429+ mockUserRepo .On ("FindByID" , mock .Anything , testUserID ).Return (tc .mockUserReturn , tc .mockUserErr ).Once ()
430+ }
431+
432+ // Mock product repo update (only if middleware/validation/parsing passes)
433+ if tc .productID != "not-a-uuid" && tc .mockUserErr == nil && tc .expectedStatus != http .StatusBadRequest && tc .expectedStatus != http .StatusUnauthorized {
434+ parsedID , _ := uuid .Parse (tc .productID )
435+ mockProductRepo .On ("Update" , mock .Anything , parsedID , mock .AnythingOfType ("*models.Product" )).
436+ Return (func (ctx context.Context , id uuid.UUID , p * models.Product ) * models.Product {
437+ if tc .mockUpdateErr != nil {
438+ return nil
439+ }
440+ p .ID = id
441+ p .UpdatedAt = time .Now () // Simulate update
442+ return p
443+ }, tc .mockUpdateErr ).Once ()
444+ }
445+
446+ req := httptest .NewRequest (http .MethodPut , "/api/products/" + tc .productID , strings .NewReader (tc .body ))
447+ req .Header .Set ("Content-Type" , "application/json" )
448+ if tc .expectedStatus != http .StatusUnauthorized || tc .expectedBody != `{"error":"authorization header required"}` {
449+ req .Header .Set ("Authorization" , "Bearer " + testToken )
450+ }
451+
452+ rr := httptest .NewRecorder ()
453+ router .ServeHTTP (rr , req )
454+
455+ assert .Equal (t , tc .expectedStatus , rr .Code )
456+ assert .Contains (t , rr .Body .String (), tc .expectedBody )
457+ mockUserRepo .AssertExpectations (t )
458+ mockProductRepo .AssertExpectations (t )
459+ })
460+ }
461+ }
462+
463+ func TestProductHandler_DeleteProduct (t * testing.T ) {
464+ // Setup inside loop
465+ testUserID := uuid .New ()
466+ productToDeleteID := uuid .New ()
467+ testJwtSecret := "test-secret-for-jwt-please-change"
468+ testToken := generateTestToken (testUserID , testJwtSecret )
469+
470+ tests := []struct {
471+ name string
472+ productID string
473+ mockUserReturn * models.User
474+ mockUserErr error
475+ mockDeleteErr error
476+ expectedStatus int
477+ expectedBody string
478+ }{
479+ {
480+ name : "Success" ,
481+ productID : productToDeleteID .String (),
482+ mockUserReturn : & models.User {ID : testUserID },
483+ mockUserErr : nil ,
484+ mockDeleteErr : nil ,
485+ expectedStatus : http .StatusNoContent ,
486+ expectedBody : "" , // No body on success
487+ },
488+ {
489+ name : "Failure - Invalid UUID" ,
490+ productID : "not-a-uuid" ,
491+ mockUserReturn : & models.User {ID : testUserID },
492+ mockUserErr : nil ,
493+ mockDeleteErr : nil ,
494+ expectedStatus : http .StatusNotFound , // Expect 404 from router
495+ expectedBody : "404 page not found" ,
496+ },
497+ {
498+ name : "Failure - Product Not Found" ,
499+ productID : productToDeleteID .String (),
500+ mockUserReturn : & models.User {ID : testUserID },
501+ mockUserErr : nil ,
502+ mockDeleteErr : products .ErrProductNotFound ,
503+ expectedStatus : http .StatusNotFound ,
504+ expectedBody : `{"error":"product not found"}` ,
505+ },
506+ {
507+ name : "Failure - Repo Delete Error" ,
508+ productID : productToDeleteID .String (),
509+ mockUserReturn : & models.User {ID : testUserID },
510+ mockUserErr : nil ,
511+ mockDeleteErr : errors .New ("db delete failed" ),
512+ expectedStatus : http .StatusInternalServerError ,
513+ expectedBody : `{"error":"failed to delete product"}` ,
514+ },
515+ {
516+ name : "Failure - Middleware User Check Fails" ,
517+ productID : productToDeleteID .String (),
518+ mockUserReturn : nil ,
519+ mockUserErr : users .ErrUserNotFound ,
520+ mockDeleteErr : nil ,
521+ expectedStatus : http .StatusUnauthorized ,
522+ expectedBody : `{"error":"user associated with token not found"}` ,
523+ },
524+ {
525+ name : "Failure - No Auth Token" ,
526+ productID : productToDeleteID .String (),
527+ mockUserReturn : nil ,
528+ mockUserErr : nil ,
529+ mockDeleteErr : nil ,
530+ expectedStatus : http .StatusUnauthorized ,
531+ expectedBody : `{"error":"authorization header required"}` ,
532+ },
533+ }
534+
535+ for _ , tc := range tests {
536+ tc := tc // Capture range variable
537+ t .Run (tc .name , func (t * testing.T ) {
538+ // Moved setup inside t.Run for isolation
539+ mockProductRepo , mockUserRepo , _ , _ , router := setupProductTest (t )
540+
541+ // Mock middleware user check
542+ if tc .expectedStatus != http .StatusUnauthorized || tc .expectedBody == `{"error":"user associated with token not found"}` {
543+ mockUserRepo .On ("FindByID" , mock .Anything , testUserID ).Return (tc .mockUserReturn , tc .mockUserErr ).Once ()
544+ }
545+
546+ // Mock product repo delete (only if middleware/parsing passes)
547+ if tc .productID != "not-a-uuid" && tc .mockUserErr == nil && tc .expectedStatus != http .StatusBadRequest && tc .expectedStatus != http .StatusUnauthorized {
548+ parsedID , _ := uuid .Parse (tc .productID )
549+ mockProductRepo .On ("Delete" , mock .Anything , parsedID ).Return (tc .mockDeleteErr ).Once ()
550+ }
551+
552+ req := httptest .NewRequest (http .MethodDelete , "/api/products/" + tc .productID , nil )
553+ if tc .expectedStatus != http .StatusUnauthorized || tc .expectedBody != `{"error":"authorization header required"}` {
554+ req .Header .Set ("Authorization" , "Bearer " + testToken )
555+ }
556+
557+ rr := httptest .NewRecorder ()
558+ router .ServeHTTP (rr , req )
559+
560+ assert .Equal (t , tc .expectedStatus , rr .Code )
561+ if tc .expectedBody != "" {
562+ assert .Contains (t , rr .Body .String (), tc .expectedBody )
563+ } else {
564+ assert .Empty (t , rr .Body .String ())
565+ }
566+ mockUserRepo .AssertExpectations (t )
567+ mockProductRepo .AssertExpectations (t )
568+ })
569+ }
570+ }
0 commit comments