From 3ae5af3e320e2db2b5f9bb9018cd9afd53cb7f05 Mon Sep 17 00:00:00 2001 From: danielyehoshua123 Date: Wed, 10 Dec 2025 00:21:13 +0200 Subject: [PATCH 1/4] basic tests and CI/CD for backend --- backend_ci.yml | 25 ++++++ main_test.go | 202 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 backend_ci.yml create mode 100644 main_test.go diff --git a/backend_ci.yml b/backend_ci.yml new file mode 100644 index 0000000..cd11a5a --- /dev/null +++ b/backend_ci.yml @@ -0,0 +1,25 @@ +name: Back-End CI + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" # or whatever you're using + + - name: Run Go tests + run: go test ./... diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..3a9c249 --- /dev/null +++ b/main_test.go @@ -0,0 +1,202 @@ +// main_test.go +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +// ---- Helper to decode JSON ---- + +func decodeJSON[T any](t *testing.T, body *httptest.ResponseRecorder, out *T) { + t.Helper() + if err := json.Unmarshal(body.Body.Bytes(), out); err != nil { + t.Fatalf("failed to decode JSON: %v\nbody=%s", err, body.Body.String()) + } +} + +// ---- Handler tests ---- + +func TestGetSchoolsHandler_OK(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/schools", nil) + rr := httptest.NewRecorder() + + // wrap with CORS, like in main() + handler := enableCORS(getSchoolsHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + // Check CORS header + if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "*" { + t.Fatalf("expected CORS header '*', got %q", got) + } + + var schools []School + decodeJSON(t, rr, &schools) + + if len(schools) == 0 { + t.Fatalf("expected at least one school, got 0") + } +} + +func TestGetGradesHandler_MissingParam(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/grades", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getGradesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected status 400 for missing school_id, got %d", rr.Code) + } +} + +func TestGetGradesHandler_ValidSchool(t *testing.T) { + // "1" is valid according to MockSchools in mock_db.go + req := httptest.NewRequest(http.MethodGet, "/api/grades?school_id=1", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getGradesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + var grades []Grade + decodeJSON(t, rr, &grades) + if len(grades) == 0 { + t.Fatalf("expected at least one grade for school_id=1") + } +} + +func TestGetGradesHandler_InvalidSchool(t *testing.T) { + // school_id=999 should return nil from GetGradesBySchoolID + req := httptest.NewRequest(http.MethodGet, "/api/grades?school_id=999", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getGradesHandler) + handler.ServeHTTP(rr, req) + + // current implementation will encode `nil` as JSON "null" with 200 OK. + // We at least check it doesn't crash and returns 200. + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200 even for invalid school (mocked), got %d", rr.Code) + } +} + +func TestGetClassesHandler_MissingParams(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/classes?school_id=1", nil) // missing grade_id + rr := httptest.NewRecorder() + + handler := enableCORS(getClassesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected status 400 for missing params, got %d", rr.Code) + } +} + +func TestGetClassesHandler_OK(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/classes?school_id=1&grade_id=9", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getClassesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + var classes []Class + decodeJSON(t, rr, &classes) + if len(classes) == 0 { + t.Fatalf("expected at least one class") + } +} + +func TestGetEquipmentListsHandler_MissingParams(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/equipment?school_id=1&grade_id=9", nil) // missing class_id + rr := httptest.NewRecorder() + + handler := enableCORS(getEquipmentListsHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected status 400 for missing params, got %d", rr.Code) + } +} + +func TestGetEquipmentListsHandler_DefaultList(t *testing.T) { + // This combination is not explicitly listed in MockEquipmentLists, so we hit "default" + req := httptest.NewRequest(http.MethodGet, "/api/equipment?school_id=1&grade_id=9&class_id=2", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getEquipmentListsHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + var equipment []Equipment + decodeJSON(t, rr, &equipment) + if len(equipment) == 0 { + t.Fatalf("expected at least one equipment item") + } +} +func TestGetSchools(t *testing.T) { + schools := GetSchools() + if len(schools) == 0 { + t.Fatalf("expected non-empty schools list") + } +} + +func TestGetGradesBySchoolID_Valid(t *testing.T) { + grades := GetGradesBySchoolID("1") // "1" exists in MockSchools + if len(grades) == 0 { + t.Fatalf("expected grades for valid school ID") + } +} + +func TestGetGradesBySchoolID_Invalid(t *testing.T) { + grades := GetGradesBySchoolID("999") + if grades != nil { + t.Fatalf("expected nil for invalid school ID, got %+v", grades) + } +} + +func TestGetClassesByGradeID_Valid(t *testing.T) { + classes := GetClassesByGradeID("1", "9") + if len(classes) == 0 { + t.Fatalf("expected classes for valid school/grade") + } +} + +func TestGetClassesByGradeID_InvalidSchool(t *testing.T) { + classes := GetClassesByGradeID("999", "9") + if classes != nil { + t.Fatalf("expected nil for invalid school ID") + } +} + +func TestGetEquipmentList_SpecificKey(t *testing.T) { + // "1-9-1" is explicitly defined in MockEquipmentLists + list := GetEquipmentList("1", "9", "1") + if len(list) == 0 { + t.Fatalf("expected non-empty list for 1-9-1") + } +} + +func TestGetEquipmentList_DefaultKey(t *testing.T) { + // some combination that is not explicitly defined → should hit "default" + list := GetEquipmentList("123", "456", "789") + if len(list) == 0 { + t.Fatalf("expected non-empty default list") + } +} \ No newline at end of file From a7f75951d57d4068e7219c7e2345bbf336be0cc8 Mon Sep 17 00:00:00 2001 From: danielyehoshua123 Date: Wed, 10 Dec 2025 01:50:17 +0200 Subject: [PATCH 2/4] divided JSON based tests from database query tests --- main_test.go | 68 -------------------------------------- mock_database_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 68 deletions(-) create mode 100644 mock_database_test.go diff --git a/main_test.go b/main_test.go index 3a9c249..759470b 100644 --- a/main_test.go +++ b/main_test.go @@ -132,71 +132,3 @@ func TestGetEquipmentListsHandler_MissingParams(t *testing.T) { } } -func TestGetEquipmentListsHandler_DefaultList(t *testing.T) { - // This combination is not explicitly listed in MockEquipmentLists, so we hit "default" - req := httptest.NewRequest(http.MethodGet, "/api/equipment?school_id=1&grade_id=9&class_id=2", nil) - rr := httptest.NewRecorder() - - handler := enableCORS(getEquipmentListsHandler) - handler.ServeHTTP(rr, req) - - if rr.Code != http.StatusOK { - t.Fatalf("expected status 200, got %d", rr.Code) - } - - var equipment []Equipment - decodeJSON(t, rr, &equipment) - if len(equipment) == 0 { - t.Fatalf("expected at least one equipment item") - } -} -func TestGetSchools(t *testing.T) { - schools := GetSchools() - if len(schools) == 0 { - t.Fatalf("expected non-empty schools list") - } -} - -func TestGetGradesBySchoolID_Valid(t *testing.T) { - grades := GetGradesBySchoolID("1") // "1" exists in MockSchools - if len(grades) == 0 { - t.Fatalf("expected grades for valid school ID") - } -} - -func TestGetGradesBySchoolID_Invalid(t *testing.T) { - grades := GetGradesBySchoolID("999") - if grades != nil { - t.Fatalf("expected nil for invalid school ID, got %+v", grades) - } -} - -func TestGetClassesByGradeID_Valid(t *testing.T) { - classes := GetClassesByGradeID("1", "9") - if len(classes) == 0 { - t.Fatalf("expected classes for valid school/grade") - } -} - -func TestGetClassesByGradeID_InvalidSchool(t *testing.T) { - classes := GetClassesByGradeID("999", "9") - if classes != nil { - t.Fatalf("expected nil for invalid school ID") - } -} - -func TestGetEquipmentList_SpecificKey(t *testing.T) { - // "1-9-1" is explicitly defined in MockEquipmentLists - list := GetEquipmentList("1", "9", "1") - if len(list) == 0 { - t.Fatalf("expected non-empty list for 1-9-1") - } -} - -func TestGetEquipmentList_DefaultKey(t *testing.T) { - // some combination that is not explicitly defined → should hit "default" - list := GetEquipmentList("123", "456", "789") - if len(list) == 0 { - t.Fatalf("expected non-empty default list") - } -} \ No newline at end of file diff --git a/mock_database_test.go b/mock_database_test.go new file mode 100644 index 0000000..fb38413 --- /dev/null +++ b/mock_database_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) +func TestGetEquipmentListsHandler_DefaultList(t *testing.T) { + // This combination is not explicitly listed in MockEquipmentLists, so we hit "default" + req := httptest.NewRequest(http.MethodGet, "/api/equipment?school_id=1&grade_id=9&class_id=2", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getEquipmentListsHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + var equipment []Equipment + decodeJSON(t, rr, &equipment) + if len(equipment) == 0 { + t.Fatalf("expected at least one equipment item") + } +} +func TestGetSchools(t *testing.T) { + schools := GetSchools() + if len(schools) == 0 { + t.Fatalf("expected non-empty schools list") + } +} + +func TestGetGradesBySchoolID_Valid(t *testing.T) { + grades := GetGradesBySchoolID("1") // "1" exists in MockSchools + if len(grades) == 0 { + t.Fatalf("expected grades for valid school ID") + } +} + +func TestGetGradesBySchoolID_Invalid(t *testing.T) { + grades := GetGradesBySchoolID("999") + if grades != nil { + t.Fatalf("expected nil for invalid school ID, got %+v", grades) + } +} + +func TestGetClassesByGradeID_Valid(t *testing.T) { + classes := GetClassesByGradeID("1", "9") + if len(classes) == 0 { + t.Fatalf("expected classes for valid school/grade") + } +} + +func TestGetClassesByGradeID_InvalidSchool(t *testing.T) { + classes := GetClassesByGradeID("999", "9") + if classes != nil { + t.Fatalf("expected nil for invalid school ID") + } +} + +func TestGetEquipmentList_SpecificKey(t *testing.T) { + // "1-9-1" is explicitly defined in MockEquipmentLists + list := GetEquipmentList("1", "9", "1") + if len(list) == 0 { + t.Fatalf("expected non-empty list for 1-9-1") + } +} + +func TestGetEquipmentList_DefaultKey(t *testing.T) { + // some combination that is not explicitly defined → should hit "default" + list := GetEquipmentList("123", "456", "789") + if len(list) == 0 { + t.Fatalf("expected non-empty default list") + } +} \ No newline at end of file From 441cb275b22f33d7be3ab0bad42cac466f5ebed1 Mon Sep 17 00:00:00 2001 From: danielyehoshua123 Date: Wed, 10 Dec 2025 02:07:54 +0200 Subject: [PATCH 3/4] added basic tests for backend, database --- .github/workflows/backend_ci.yml | 25 +++++ .github/workflows/main_test.go | 134 ++++++++++++++++++++++++ .github/workflows/mock_database_test.go | 76 ++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 .github/workflows/backend_ci.yml create mode 100644 .github/workflows/main_test.go create mode 100644 .github/workflows/mock_database_test.go diff --git a/.github/workflows/backend_ci.yml b/.github/workflows/backend_ci.yml new file mode 100644 index 0000000..cd11a5a --- /dev/null +++ b/.github/workflows/backend_ci.yml @@ -0,0 +1,25 @@ +name: Back-End CI + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" # or whatever you're using + + - name: Run Go tests + run: go test ./... diff --git a/.github/workflows/main_test.go b/.github/workflows/main_test.go new file mode 100644 index 0000000..759470b --- /dev/null +++ b/.github/workflows/main_test.go @@ -0,0 +1,134 @@ +// main_test.go +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +// ---- Helper to decode JSON ---- + +func decodeJSON[T any](t *testing.T, body *httptest.ResponseRecorder, out *T) { + t.Helper() + if err := json.Unmarshal(body.Body.Bytes(), out); err != nil { + t.Fatalf("failed to decode JSON: %v\nbody=%s", err, body.Body.String()) + } +} + +// ---- Handler tests ---- + +func TestGetSchoolsHandler_OK(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/schools", nil) + rr := httptest.NewRecorder() + + // wrap with CORS, like in main() + handler := enableCORS(getSchoolsHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + // Check CORS header + if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "*" { + t.Fatalf("expected CORS header '*', got %q", got) + } + + var schools []School + decodeJSON(t, rr, &schools) + + if len(schools) == 0 { + t.Fatalf("expected at least one school, got 0") + } +} + +func TestGetGradesHandler_MissingParam(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/grades", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getGradesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected status 400 for missing school_id, got %d", rr.Code) + } +} + +func TestGetGradesHandler_ValidSchool(t *testing.T) { + // "1" is valid according to MockSchools in mock_db.go + req := httptest.NewRequest(http.MethodGet, "/api/grades?school_id=1", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getGradesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + var grades []Grade + decodeJSON(t, rr, &grades) + if len(grades) == 0 { + t.Fatalf("expected at least one grade for school_id=1") + } +} + +func TestGetGradesHandler_InvalidSchool(t *testing.T) { + // school_id=999 should return nil from GetGradesBySchoolID + req := httptest.NewRequest(http.MethodGet, "/api/grades?school_id=999", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getGradesHandler) + handler.ServeHTTP(rr, req) + + // current implementation will encode `nil` as JSON "null" with 200 OK. + // We at least check it doesn't crash and returns 200. + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200 even for invalid school (mocked), got %d", rr.Code) + } +} + +func TestGetClassesHandler_MissingParams(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/classes?school_id=1", nil) // missing grade_id + rr := httptest.NewRecorder() + + handler := enableCORS(getClassesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected status 400 for missing params, got %d", rr.Code) + } +} + +func TestGetClassesHandler_OK(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/classes?school_id=1&grade_id=9", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getClassesHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + var classes []Class + decodeJSON(t, rr, &classes) + if len(classes) == 0 { + t.Fatalf("expected at least one class") + } +} + +func TestGetEquipmentListsHandler_MissingParams(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/equipment?school_id=1&grade_id=9", nil) // missing class_id + rr := httptest.NewRecorder() + + handler := enableCORS(getEquipmentListsHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected status 400 for missing params, got %d", rr.Code) + } +} + diff --git a/.github/workflows/mock_database_test.go b/.github/workflows/mock_database_test.go new file mode 100644 index 0000000..fb38413 --- /dev/null +++ b/.github/workflows/mock_database_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) +func TestGetEquipmentListsHandler_DefaultList(t *testing.T) { + // This combination is not explicitly listed in MockEquipmentLists, so we hit "default" + req := httptest.NewRequest(http.MethodGet, "/api/equipment?school_id=1&grade_id=9&class_id=2", nil) + rr := httptest.NewRecorder() + + handler := enableCORS(getEquipmentListsHandler) + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rr.Code) + } + + var equipment []Equipment + decodeJSON(t, rr, &equipment) + if len(equipment) == 0 { + t.Fatalf("expected at least one equipment item") + } +} +func TestGetSchools(t *testing.T) { + schools := GetSchools() + if len(schools) == 0 { + t.Fatalf("expected non-empty schools list") + } +} + +func TestGetGradesBySchoolID_Valid(t *testing.T) { + grades := GetGradesBySchoolID("1") // "1" exists in MockSchools + if len(grades) == 0 { + t.Fatalf("expected grades for valid school ID") + } +} + +func TestGetGradesBySchoolID_Invalid(t *testing.T) { + grades := GetGradesBySchoolID("999") + if grades != nil { + t.Fatalf("expected nil for invalid school ID, got %+v", grades) + } +} + +func TestGetClassesByGradeID_Valid(t *testing.T) { + classes := GetClassesByGradeID("1", "9") + if len(classes) == 0 { + t.Fatalf("expected classes for valid school/grade") + } +} + +func TestGetClassesByGradeID_InvalidSchool(t *testing.T) { + classes := GetClassesByGradeID("999", "9") + if classes != nil { + t.Fatalf("expected nil for invalid school ID") + } +} + +func TestGetEquipmentList_SpecificKey(t *testing.T) { + // "1-9-1" is explicitly defined in MockEquipmentLists + list := GetEquipmentList("1", "9", "1") + if len(list) == 0 { + t.Fatalf("expected non-empty list for 1-9-1") + } +} + +func TestGetEquipmentList_DefaultKey(t *testing.T) { + // some combination that is not explicitly defined → should hit "default" + list := GetEquipmentList("123", "456", "789") + if len(list) == 0 { + t.Fatalf("expected non-empty default list") + } +} \ No newline at end of file From 9ecec85fa2a64941998471149e09627de9fa0356 Mon Sep 17 00:00:00 2001 From: danielyehoshua123 Date: Wed, 10 Dec 2025 02:26:32 +0200 Subject: [PATCH 4/4] minor bug fix so that pull request could work --- go.mod | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..09a5931 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module back-end + +go 1.25.4