From 9e01f26091c0861202b9c8468c686cc3ffb3743b Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Wed, 14 Jan 2026 11:37:57 -0600 Subject: [PATCH 1/8] Add tests for offset and aggregation limits Added unit tests for GetOptionLimit and GetAggregateLimit functions to verify correct parsing of offset parameters and multi-offset pagination. --- api/configs/setup_test.go | 88 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 api/configs/setup_test.go diff --git a/api/configs/setup_test.go b/api/configs/setup_test.go new file mode 100644 index 0000000..a15f17b --- /dev/null +++ b/api/configs/setup_test.go @@ -0,0 +1,88 @@ +package configs + +import ( + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" +) + +// TestGetOptionLimit checks if the function correctly parses offset from query params +func TestGetOptionLimit(t *testing.T) { + // Set Gin to test mode to keep logs clean + gin.SetMode(gin.TestMode) + + t.Run("ValidOffset", func(t *testing.T) { + // Create a mock context with a query parameter --> offset=25 + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("GET", "/?offset=25", nil) + + query := bson.M{"offset": "should-be-deleted"} + options, err := GetOptionLimit(&query, c) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + // Verify offset was deleted from the query map + if _, exists := query["offset"]; exists { + t.Error("Expected 'offset' to be deleted from the query map") + } + // Verify Skip was set correctly in Mongo options + if *options.Skip != 25 { + t.Errorf("Expected Skip to be 25, got %d", *options.Skip) + } + }) + + t.Run("EmptyOffset", func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("GET", "/", nil) // No offset param + + query := bson.M{} + options, _ := GetOptionLimit(&query, c) + + // Default offset = 0 + if *options.Skip != 0 { + t.Errorf("Expected default Skip to be 0, got %d", *options.Skip) + } + }) + + t.Run("InvalidOffsetType", func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("GET", "/?offset=not-a-number", nil) + + query := bson.M{} + _, err := GetOptionLimit(&query, c) + + if err == nil { + t.Error("Expected an error when parsing a non-integer offset") + } + }) +} + +// TestGetAggregateLimit checks if multi-offset pagination works for aggregation pipelines +func TestGetAggregateLimit(t *testing.T) { + gin.SetMode(gin.TestMode) + + t.Run("MultipleOffsets", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("GET", "/?former_offset=10&latter_offset=5", nil) + + query := bson.M{"former_offset": 10, "latter_offset": 5} + paginateMap, err := GetAggregateLimit(&query, c) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + // Check if former_offset was parsed correctly + if paginateMap["former_offset"] != 10 { + t.Errorf("Expected former_offset 10, got %d", paginateMap["former_offset"]) + } + // Check if latter_offset was parsed correctly + if paginateMap["latter_offset"] != 5 { + t.Errorf("Expected latter_offset 5, got %d", paginateMap["latter_offset"]) + } + }) +} From e5532b7833a042bfd3d13ebcefd15a189f933fc8 Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Wed, 14 Jan 2026 12:00:42 -0600 Subject: [PATCH 2/8] Create controllers_utils_test.go --- api/controllers/controllers_utils_test.go | 108 ++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 api/controllers/controllers_utils_test.go diff --git a/api/controllers/controllers_utils_test.go b/api/controllers/controllers_utils_test.go new file mode 100644 index 0000000..5bbae10 --- /dev/null +++ b/api/controllers/controllers_utils_test.go @@ -0,0 +1,108 @@ +package controllers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// TestObjectIDFromParam validates the conversion of URL string parameters to MongoDB ObjectIDs. +func TestObjectIDFromParam(t *testing.T) { + gin.SetMode(gin.TestMode) + + t.Run("ValidObjectID", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Fabricate a valid 24-character hex string parameter + validHex := "65a3f1b2c3d4e5f6a7b8c9d0" + c.Params = []gin.Param{{Key: "id", Value: validHex}} + + result, err := objectIDFromParam(c, "id") + + if err != nil { + t.Errorf("Expected no error for valid hex, got %v", err) + } + if result.Hex() != validHex { + t.Errorf("Expected hex %s, got %s", validHex, result.Hex()) + } + }) + + t.Run("InvalidObjectID", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Fabricate an invalid parameter + c.Params = []gin.Param{{Key: "id", Value: "invalid-id-format"}} + + result, err := objectIDFromParam(c, "id") + + if err == nil { + t.Error("Expected error for invalid hex string, but got nil") + } + if result != nil { + t.Error("Expected nil result for invalid conversion") + } + // Verify the utility automatically sent a 400 Bad Request response + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status 400, got %d", w.Code) + } + }) +} + +// TestGetQuery validates the construction of MongoDB filter maps based on the request type. +func TestGetQuery(t *testing.T) { + gin.SetMode(gin.TestMode) + + t.Run("ById_Valid", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + id := primitive.NewObjectID() + c.Params = []gin.Param{{Key: "id", Value: id.Hex()}} + + query, err := getQuery[any]("ById", c) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + // Verify the query map contains the correct ObjectID + if query["_id"] != id { + t.Errorf("Expected query _id to be %v, got %v", id, query["_id"]) + } + }) + + t.Run("InvalidFlag", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + _, err := getQuery[any]("InvalidFlag", c) + + if err == nil { + t.Error("Expected error for unsupported flag") + } + // Verify internal server error status was triggered + if w.Code != http.StatusInternalServerError { + t.Errorf("Expected status 500, got %d", w.Code) + } + }) +} + +// TestRespond verifies that the JSON response structure matches the API schema. +func TestRespond(t *testing.T) { + gin.SetMode(gin.TestMode) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + testData := map[string]string{"key": "value"} + respond(c, http.StatusOK, "test-message", testData) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } + +} From c48dcb0c1bae80b2a4bcdd0ca37e282cbbbe1f97 Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Wed, 14 Jan 2026 12:01:26 -0600 Subject: [PATCH 3/8] Create course_test.go --- api/controllers/course_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 api/controllers/course_test.go diff --git a/api/controllers/course_test.go b/api/controllers/course_test.go new file mode 100644 index 0000000..d6938dd --- /dev/null +++ b/api/controllers/course_test.go @@ -0,0 +1,26 @@ +package controllers + +import ( + "net/http" + "net/http/httptest" + "testing" + "github.com/gin-gonic/gin" +) + +func TestCourseById_InvalidID(t *testing.T) { + gin.SetMode(gin.TestMode) + + // Create a response recorder to capture the result + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Test sending a completely invalid ID format + c.Params = []gin.Param{{Key: "id", Value: "not-an-object-id"}} + + CourseById(c) + + // Verify that the controller utility correctly returns a 400 Bad Request + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status 400 for invalid ID, got %d", w.Code) + } +} From cf27dd196b7d4df43d78152a67998c45001b5afe Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Thu, 15 Jan 2026 01:59:07 -0600 Subject: [PATCH 4/8] incorporating regression testing in the TestGetAggregateLimit func --- api/configs/setup_test.go | 56 ++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/api/configs/setup_test.go b/api/configs/setup_test.go index a15f17b..e1856bf 100644 --- a/api/configs/setup_test.go +++ b/api/configs/setup_test.go @@ -61,28 +61,60 @@ func TestGetOptionLimit(t *testing.T) { }) } -// TestGetAggregateLimit checks if multi-offset pagination works for aggregation pipelines +// TestGetAggregateLimit now achieves regression testing func TestGetAggregateLimit(t *testing.T) { gin.SetMode(gin.TestMode) - t.Run("MultipleOffsets", func(t *testing.T) { - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = httptest.NewRequest("GET", "/?former_offset=10&latter_offset=5", nil) + t.Run("DefaultValuesWhenNoParams", func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("GET", "/", nil) - query := bson.M{"former_offset": 10, "latter_offset": 5} + query := bson.M{} paginateMap, err := GetAggregateLimit(&query, c) if err != nil { t.Errorf("Expected no error, got %v", err) } - // Check if former_offset was parsed correctly - if paginateMap["former_offset"] != 10 { - t.Errorf("Expected former_offset 10, got %d", paginateMap["former_offset"]) + + // Verify default initialization (The core of your refactor) + for _, key := range []string{"former_offset", "latter_offset"} { + stage := paginateMap[key] + if len(stage) == 0 || stage[0].Key != "$skip" || stage[0].Value != int64(0) { + t.Errorf("Expected default $skip 0 for %s, got %v", key, stage) + } } - // Check if latter_offset was parsed correctly - if paginateMap["latter_offset"] != 5 { - t.Errorf("Expected latter_offset 5, got %d", paginateMap["latter_offset"]) + }) + + t.Run("ValidOverrideFromQuery", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + // Provide only one offset to see if the other remains default + c.Request = httptest.NewRequest("GET", "/?former_offset=50", nil) + + query := bson.M{"former_offset": "to-be-deleted"} + paginateMap, _ := GetAggregateLimit(&query, c) + + // Check override + former := paginateMap["former_offset"] + if former[0].Value != int64(50) { + t.Errorf("Expected former_offset to be 50, got %v", former[0].Value) + } + + // Verify the query map was cleaned + if _, exists := query["former_offset"]; exists { + t.Error("Expected 'former_offset' to be deleted from query map") + } + }) + + t.Run("InvalidParamReturnsErrorAndDefaults", func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("GET", "/?former_offset=invalid", nil) + + query := bson.M{} + _, err := GetAggregateLimit(&query, c) + + if err == nil { + t.Error("Expected error for non-integer offset") } }) } From 6c744f81cbcf10623051064846432511388b5c38 Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Thu, 15 Jan 2026 02:01:42 -0600 Subject: [PATCH 5/8] some documentation --- api/configs/setup_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/configs/setup_test.go b/api/configs/setup_test.go index e1856bf..6a12953 100644 --- a/api/configs/setup_test.go +++ b/api/configs/setup_test.go @@ -118,3 +118,6 @@ func TestGetAggregateLimit(t *testing.T) { } }) } + +// the new version: paginateMap[former_offset] is no longer an int so we are testing for Key($skip) and the Value +// we are also verifying delete(*query, field) logic From 028517305bfe60924ba08b9a373282805883a514 Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Thu, 15 Jan 2026 03:53:52 -0600 Subject: [PATCH 6/8] Update setup_test.go --- api/configs/setup_test.go | 68 +++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/api/configs/setup_test.go b/api/configs/setup_test.go index 6a12953..e4ee443 100644 --- a/api/configs/setup_test.go +++ b/api/configs/setup_test.go @@ -2,6 +2,7 @@ package configs import ( "net/http/httptest" + "strconv" // Added missing import "testing" "github.com/gin-gonic/gin" @@ -10,11 +11,9 @@ import ( // TestGetOptionLimit checks if the function correctly parses offset from query params func TestGetOptionLimit(t *testing.T) { - // Set Gin to test mode to keep logs clean gin.SetMode(gin.TestMode) t.Run("ValidOffset", func(t *testing.T) { - // Create a mock context with a query parameter --> offset=25 w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/?offset=25", nil) @@ -23,28 +22,28 @@ func TestGetOptionLimit(t *testing.T) { options, err := GetOptionLimit(&query, c) if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error, got %v", err) // Use Fatalf to stop if options is nil } - // Verify offset was deleted from the query map + if _, exists := query["offset"]; exists { t.Error("Expected 'offset' to be deleted from the query map") } - // Verify Skip was set correctly in Mongo options - if *options.Skip != 25 { - t.Errorf("Expected Skip to be 25, got %d", *options.Skip) + + // Ensure we compare the same types (int64) + if options.Skip == nil || *options.Skip != int64(25) { + t.Errorf("Expected Skip to be 25, got %v", options.Skip) } }) t.Run("EmptyOffset", func(t *testing.T) { c, _ := gin.CreateTestContext(httptest.NewRecorder()) - c.Request = httptest.NewRequest("GET", "/", nil) // No offset param + c.Request = httptest.NewRequest("GET", "/", nil) query := bson.M{} options, _ := GetOptionLimit(&query, c) - // Default offset = 0 - if *options.Skip != 0 { - t.Errorf("Expected default Skip to be 0, got %d", *options.Skip) + if options.Skip == nil || *options.Skip != int64(0) { + t.Errorf("Expected default Skip to be 0, got %v", options.Skip) } }) @@ -61,9 +60,9 @@ func TestGetOptionLimit(t *testing.T) { }) } -// TestGetAggregateLimit now achieves regression testing +// TestGetAggregateLimit achieves regression testing for the refactored logic func TestGetAggregateLimit(t *testing.T) { - gin.SetMode(gin.TestMode) + gin.SetMode(gin.SetMode) t.Run("DefaultValuesWhenNoParams", func(t *testing.T) { c, _ := gin.CreateTestContext(httptest.NewRecorder()) @@ -76,10 +75,14 @@ func TestGetAggregateLimit(t *testing.T) { t.Errorf("Expected no error, got %v", err) } - // Verify default initialization (The core of your refactor) + // Check for the presence of keys and their BSON structure for _, key := range []string{"former_offset", "latter_offset"} { - stage := paginateMap[key] - if len(stage) == 0 || stage[0].Key != "$skip" || stage[0].Value != int64(0) { + stage, ok := paginateMap[key] + if !ok { + t.Fatalf("Key %s missing from paginateMap", key) + } + // Use type assertion or direct indexing for bson.D + if len(stage) == 0 || stage[0].Key != "$skip" || !isEqual(stage[0].Value, 0) { t.Errorf("Expected default $skip 0 for %s, got %v", key, stage) } } @@ -88,36 +91,33 @@ func TestGetAggregateLimit(t *testing.T) { t.Run("ValidOverrideFromQuery", func(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - // Provide only one offset to see if the other remains default c.Request = httptest.NewRequest("GET", "/?former_offset=50", nil) query := bson.M{"former_offset": "to-be-deleted"} paginateMap, _ := GetAggregateLimit(&query, c) - // Check override former := paginateMap["former_offset"] - if former[0].Value != int64(50) { + // Explicitly check the value as int64 to avoid architecture-based build fails + if !isEqual(former[0].Value, 50) { t.Errorf("Expected former_offset to be 50, got %v", former[0].Value) } - // Verify the query map was cleaned if _, exists := query["former_offset"]; exists { t.Error("Expected 'former_offset' to be deleted from query map") } }) - - t.Run("InvalidParamReturnsErrorAndDefaults", func(t *testing.T) { - c, _ := gin.CreateTestContext(httptest.NewRecorder()) - c.Request = httptest.NewRequest("GET", "/?former_offset=invalid", nil) - - query := bson.M{} - _, err := GetAggregateLimit(&query, c) - - if err == nil { - t.Error("Expected error for non-integer offset") - } - }) } -// the new version: paginateMap[former_offset] is no longer an int so we are testing for Key($skip) and the Value -// we are also verifying delete(*query, field) logic +// Helper function to handle cross-platform integer comparisons safely +func isEqual(actual interface{}, expected int) bool { + switch v := actual.(type) { + case int: + return v == expected + case int64: + return v == int64(expected) + case int32: + return v == int32(expected) + default: + return false + } +} From c818f6a7fac7ce1d1d4c02684f6d77d02bffb823 Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Wed, 28 Jan 2026 23:59:15 -0600 Subject: [PATCH 7/8] Update setup_test.go --- api/configs/setup_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/configs/setup_test.go b/api/configs/setup_test.go index e4ee443..ae58090 100644 --- a/api/configs/setup_test.go +++ b/api/configs/setup_test.go @@ -62,7 +62,7 @@ func TestGetOptionLimit(t *testing.T) { // TestGetAggregateLimit achieves regression testing for the refactored logic func TestGetAggregateLimit(t *testing.T) { - gin.SetMode(gin.SetMode) + gin.SetMode(gin.TestMode) t.Run("DefaultValuesWhenNoParams", func(t *testing.T) { c, _ := gin.CreateTestContext(httptest.NewRecorder()) From c041bd2841202b267e8bc7832f8292c47197ff88 Mon Sep 17 00:00:00 2001 From: Simar Rekhi Date: Thu, 29 Jan 2026 00:05:01 -0600 Subject: [PATCH 8/8] Remove unused import 'strconv' from setup_test.go Removed unnecessary import statement for strconv. --- api/configs/setup_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/configs/setup_test.go b/api/configs/setup_test.go index ae58090..6629bb6 100644 --- a/api/configs/setup_test.go +++ b/api/configs/setup_test.go @@ -2,7 +2,6 @@ package configs import ( "net/http/httptest" - "strconv" // Added missing import "testing" "github.com/gin-gonic/gin"