package router_test import ( "context" "database/sql" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "git.godopu.com/lab/landing_page/backend/internal/db" "git.godopu.com/lab/landing_page/backend/internal/models" "git.godopu.com/lab/landing_page/backend/internal/router" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" _ "modernc.org/sqlite" ) func setupTestDB(t *testing.T) *sql.DB { // In-memory or temporary file sqlite DB tmpDB := filepath.Join(t.TempDir(), "test.db") database, err := db.Connect(tmpDB) require.NoError(t, err) err = db.RunMigrations(database) require.NoError(t, err) // Seed test data from ../../seed/data seedDir := filepath.Join("..", "..", "seed", "data") seedDatabase(t, database, seedDir) return database } func seedDatabase(t *testing.T, database *sql.DB, seedDir string) { tx, err := database.BeginTx(context.Background(), nil) require.NoError(t, err) defer tx.Rollback() // 1. Projects pData, err := os.ReadFile(filepath.Join(seedDir, "research_projects.json")) require.NoError(t, err) var projects []struct { Slug string `json:"slug"` Title string `json:"title"` Abstract string `json:"abstract"` Keywords []string `json:"keywords"` StandardsTrack *string `json:"standardsTrack"` StandardsOrg *string `json:"standardsOrg"` Period *string `json:"period"` Funder *string `json:"funder"` } require.NoError(t, json.Unmarshal(pData, &projects)) for _, p := range projects { kwJSON, _ := json.Marshal(p.Keywords) _, err := tx.Exec(` INSERT INTO research_projects (slug, title, abstract, keywords, organization, standards_org, period, funder) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, p.Slug, p.Title, p.Abstract, string(kwJSON), p.StandardsTrack, p.StandardsOrg, p.Period, p.Funder) require.NoError(t, err) } // 2. Areas aData, err := os.ReadFile(filepath.Join(seedDir, "research_areas.json")) require.NoError(t, err) var areas []models.ResearchArea require.NoError(t, json.Unmarshal(aData, &areas)) for _, a := range areas { _, err := tx.Exec(`INSERT INTO research_areas (name_en, name_kr, display_order) VALUES (?, ?, ?)`, a.NameEn, a.NameKr, a.DisplayOrder) require.NoError(t, err) } // 3. Members mData, err := os.ReadFile(filepath.Join(seedDir, "members.json")) require.NoError(t, err) var members []models.Member require.NoError(t, json.Unmarshal(mData, &members)) for _, m := range members { _, err := tx.Exec(` INSERT INTO members (name_ko, name_en, degree, affiliation, email, is_advisor, display_order) VALUES (?, ?, ?, ?, ?, ?, ?) `, m.NameKo, m.NameEn, m.Degree, m.Affiliation, m.Email, m.IsAdvisor, m.DisplayOrder) require.NoError(t, err) } // 4. Alumni alumniData, err := os.ReadFile(filepath.Join(seedDir, "alumni.json")) require.NoError(t, err) var alumni []models.Alumnus require.NoError(t, json.Unmarshal(alumniData, &alumni)) for _, a := range alumni { _, err := tx.Exec(` INSERT INTO alumni (name_ko, name_en, degree, graduated_at, major, current_position) VALUES (?, ?, ?, ?, ?, ?) `, a.NameKo, a.NameEn, a.Degree, a.GraduatedAt, a.Major, a.CurrentPosition) require.NoError(t, err) } // 5. Publications pubData, err := os.ReadFile(filepath.Join(seedDir, "publications.json")) require.NoError(t, err) var pubs []models.Publication require.NoError(t, json.Unmarshal(pubData, &pubs)) for _, p := range pubs { _, err := tx.Exec(` INSERT INTO publications (category, title, authors, venue, volume, published_at, kci, doi, is_highlight) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, p.Category, p.Title, p.Authors, p.Venue, p.Volume, p.PublishedAt, p.KCI, p.DOI, p.IsHighlight) require.NoError(t, err) } // 6. Patents patData, err := os.ReadFile(filepath.Join(seedDir, "patents.json")) require.NoError(t, err) var patents []models.Patent require.NoError(t, json.Unmarshal(patData, &patents)) for _, pat := range patents { _, err := tx.Exec(` INSERT INTO patents (title, inventors, application_no, application_at, registration_no, registration_at, country, published_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, pat.Title, pat.Inventors, pat.ApplicationNo, pat.ApplicationAt, pat.RegistrationNo, pat.RegistrationAt, pat.Country, pat.PublishedAt) require.NoError(t, err) } // 7. Semesters & Courses lecData, err := os.ReadFile(filepath.Join(seedDir, "lectures.json")) require.NoError(t, err) var semesters []struct { Term string `json:"term"` Year int `json:"year"` Courses []struct { Code string `json:"code"` En string `json:"en"` Note *string `json:"note"` } `json:"courses"` } require.NoError(t, json.Unmarshal(lecData, &semesters)) for _, s := range semesters { res, err := tx.Exec(`INSERT INTO semesters (year, term) VALUES (?, ?)`, s.Year, s.Term) require.NoError(t, err) semID, _ := res.LastInsertId() for cIdx, c := range s.Courses { _, err := tx.Exec(` INSERT INTO courses (semester_id, code, name_en, name_kr, note, display_order) VALUES (?, ?, ?, ?, ?, ?) `, semID, c.Code, c.En, nil, c.Note, cIdx+1) require.NoError(t, err) } } // 8. Standards stdData, err := os.ReadFile(filepath.Join(seedDir, "standards.json")) require.NoError(t, err) var standards []struct { Org string `json:"org"` Full string `json:"full"` Scope string `json:"scope"` Period string `json:"period"` Role *string `json:"role"` Projects []struct { WG string `json:"wg"` Name string `json:"name"` Documents []struct { Title string `json:"title"` Ref string `json:"ref"` Status string `json:"status"` PublishedAt *string `json:"publishedAt"` } `json:"documents"` } `json:"projects"` } require.NoError(t, json.Unmarshal(stdData, &standards)) for bIdx, b := range standards { res, err := tx.Exec(` INSERT INTO standards_bodies (org, full_name, scope, period, role, display_order) VALUES (?, ?, ?, ?, ?, ?) `, b.Org, b.Full, b.Scope, b.Period, b.Role, bIdx+1) require.NoError(t, err) bodyID, _ := res.LastInsertId() for _, p := range b.Projects { for _, d := range p.Documents { _, err := tx.Exec(` INSERT INTO standard_documents (body_id, wg, project_name, title, doc_ref, status, published_at) VALUES (?, ?, ?, ?, ?, ?, ?) `, bodyID, p.WG, p.Name, d.Title, d.Ref, d.Status, d.PublishedAt) require.NoError(t, err) } } } require.NoError(t, tx.Commit()) } type APIResponse[T any] struct { Data T `json:"data"` Error *struct { Message string `json:"message"` } `json:"error"` } func executeRequest(r http.Handler, method, target string) *httptest.ResponseRecorder { req := httptest.NewRequest(method, target, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func TestHealthEndpoint(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) w := executeRequest(r, "GET", "/api/v1/health") assert.Equal(t, http.StatusOK, w.Code) var resp APIResponse[map[string]any] err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, "ok", resp.Data["status"]) assert.Equal(t, "anl-backend-api", resp.Data["service"]) } func TestResearchProjects(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) w := executeRequest(r, "GET", "/api/v1/research-projects") assert.Equal(t, http.StatusOK, w.Code) var resp APIResponse[[]models.ResearchProject] require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Len(t, resp.Data, 3) assert.Equal(t, "iot-standards", resp.Data[0].Slug) assert.Equal(t, "AIoT & AI-RAN", resp.Data[0].Title) } func TestResearchAreas(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) w := executeRequest(r, "GET", "/api/v1/research-areas") assert.Equal(t, http.StatusOK, w.Code) var resp APIResponse[[]models.ResearchArea] require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Len(t, resp.Data, 14) assert.Equal(t, 1, resp.Data[0].DisplayOrder) assert.Equal(t, "Quick UDP Internet Connection (QUIC)", resp.Data[0].NameEn) } func TestStatsSummary(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) w := executeRequest(r, "GET", "/api/v1/stats/summary") assert.Equal(t, http.StatusOK, w.Code) var resp APIResponse[models.StatsSummary] require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, 68, resp.Data.IntlPublications) assert.Equal(t, 44, resp.Data.StandardizationDocs) assert.Equal(t, 75, resp.Data.Patents) } func TestMembersAndAlumni(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) // Members wMem := executeRequest(r, "GET", "/api/v1/members") assert.Equal(t, http.StatusOK, wMem.Code) var respMem APIResponse[[]models.Member] require.NoError(t, json.Unmarshal(wMem.Body.Bytes(), &respMem)) assert.Len(t, respMem.Data, 16) assert.True(t, respMem.Data[0].IsAdvisor) assert.Equal(t, "Seok-Joo Koh", respMem.Data[0].NameEn) // Alumni wAlumni := executeRequest(r, "GET", "/api/v1/alumni") assert.Equal(t, http.StatusOK, wAlumni.Code) var respAlumni APIResponse[[]models.Alumnus] require.NoError(t, json.Unmarshal(wAlumni.Body.Bytes(), &respAlumni)) assert.Len(t, respAlumni.Data, 60) assert.GreaterOrEqual(t, respAlumni.Data[0].GraduatedAt, respAlumni.Data[1].GraduatedAt) } func TestPublicationsAndHighlights(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) // Full Publications wAll := executeRequest(r, "GET", "/api/v1/publications") assert.Equal(t, http.StatusOK, wAll.Code) var respAll APIResponse[[]models.Publication] require.NoError(t, json.Unmarshal(wAll.Body.Bytes(), &respAll)) assert.Len(t, respAll.Data, 148) // Category filter: intl wIntl := executeRequest(r, "GET", "/api/v1/publications?category=intl-journal-conf") assert.Equal(t, http.StatusOK, wIntl.Code) var respIntl APIResponse[[]models.Publication] require.NoError(t, json.Unmarshal(wIntl.Body.Bytes(), &respIntl)) assert.Len(t, respIntl.Data, 68) // Category filter: domestic wDom := executeRequest(r, "GET", "/api/v1/publications?category=domestic-journal-conf") assert.Equal(t, http.StatusOK, wDom.Code) var respDom APIResponse[[]models.Publication] require.NoError(t, json.Unmarshal(wDom.Body.Bytes(), &respDom)) assert.Len(t, respDom.Data, 80) // Invalid category wInvalid := executeRequest(r, "GET", "/api/v1/publications?category=unknown") assert.Equal(t, http.StatusBadRequest, wInvalid.Code) // Highlights (asserts exactly the 5 known representative titles) wHL := executeRequest(r, "GET", "/api/v1/publications/highlights") assert.Equal(t, http.StatusOK, wHL.Code) var respHL APIResponse[[]models.Publication] require.NoError(t, json.Unmarshal(wHL.Body.Bytes(), &respHL)) assert.Len(t, respHL.Data, 5) expectedTitles := map[string]bool{ "Globally Integrated Trust Authority (GITA) for Resource-Constrained Edge Devices in IoT and 6G": true, "Application Level Trust Authority (APPLETA) for Resource-Constrained Edge Devices in IoT and 6G": true, "mQUIC: Use of QUIC for Handover Support with Connection Migration in Wireless/Mobile Networks": true, "Use of QUIC for CoAP Transport in IoT Networks": true, "Use of QUIC for Mobile-Oriented Future Internet (Q-MOFI)": true, } for _, hl := range respHL.Data { assert.True(t, hl.IsHighlight) assert.True(t, expectedTitles[hl.Title], "Unexpected highlight title: %s", hl.Title) } // Patents wPat := executeRequest(r, "GET", "/api/v1/patents") assert.Equal(t, http.StatusOK, wPat.Code) var respPat APIResponse[[]models.Patent] require.NoError(t, json.Unmarshal(wPat.Body.Bytes(), &respPat)) assert.Len(t, respPat.Data, 75) } func TestSemestersAndCourses(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) w := executeRequest(r, "GET", "/api/v1/semesters") assert.Equal(t, http.StatusOK, w.Code) var resp APIResponse[[]models.SemesterWithCourses] require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Len(t, resp.Data, 45) // Check semester ordering (Year DESC, Fall before Spring within same year) first := resp.Data[0] assert.Equal(t, 2026, first.Year) assert.Equal(t, "Spring", first.Term) assert.NotEmpty(t, first.Courses) // In 2025: Fall must come before Spring var s2025Fall, s2025Spring int for idx, s := range resp.Data { if s.Year == 2025 && s.Term == "Fall" { s2025Fall = idx } if s.Year == 2025 && s.Term == "Spring" { s2025Spring = idx } } assert.Less(t, s2025Fall, s2025Spring, "2025 Fall should precede 2025 Spring in chronological descending order") } func TestStandardsBodiesAndHierarchy(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) w := executeRequest(r, "GET", "/api/v1/standards-bodies") assert.Equal(t, http.StatusOK, w.Code) var resp APIResponse[[]models.StandardsBodyWithProjects] require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Len(t, resp.Data, 6) // First body: IEC TC100 iec := resp.Data[0] assert.Equal(t, "IEC TC100", iec.Org) require.NotEmpty(t, iec.Projects) // Verify project ordering within IEC TC100 (deterministic order: WG12 before WG11(Convenor)) assert.Equal(t, "WG12", iec.Projects[0].WG) assert.Equal(t, "WG11(Convenor)", iec.Projects[1].WG) assert.NotEmpty(t, iec.Projects[0].Documents) } func TestJSONKeyCasingSnakeCase(t *testing.T) { database := setupTestDB(t) defer database.Close() r := router.SetupRouter(database) // Test Members endpoint raw JSON map wMem := executeRequest(r, "GET", "/api/v1/members") assert.Equal(t, http.StatusOK, wMem.Code) var rawMem map[string]any require.NoError(t, json.Unmarshal(wMem.Body.Bytes(), &rawMem)) dataList, ok := rawMem["data"].([]any) require.True(t, ok) require.NotEmpty(t, dataList) firstMember, ok := dataList[0].(map[string]any) require.True(t, ok) // Verify snake_case keys exist and camelCase keys do not assert.Contains(t, firstMember, "name_ko") assert.Contains(t, firstMember, "name_en") assert.Contains(t, firstMember, "is_advisor") assert.Contains(t, firstMember, "display_order") assert.NotContains(t, firstMember, "nameKo") assert.NotContains(t, firstMember, "isAdvisor") // Test Highlights endpoint raw JSON map wHL := executeRequest(r, "GET", "/api/v1/publications/highlights") assert.Equal(t, http.StatusOK, wHL.Code) var rawHL map[string]any require.NoError(t, json.Unmarshal(wHL.Body.Bytes(), &rawHL)) hlList, ok := rawHL["data"].([]any) require.True(t, ok) require.NotEmpty(t, hlList) firstHL, ok := hlList[0].(map[string]any) require.True(t, ok) assert.Contains(t, firstHL, "is_highlight") assert.Contains(t, firstHL, "published_at") assert.NotContains(t, firstHL, "isHighlight") assert.NotContains(t, firstHL, "publishedAt") }