Files
landing_page/backend/internal/router/router_test.go
T
Godopu fb6881070c feat(backend,docker,docs): implement Go/Gin/SQLite REST API server, Docker containerization, and architecture specifications
- Implement standalone Go backend API server using Gin and pure-Go SQLite (modernc.org/sqlite)
- Add 11-table schema DDL migrations (000001_init.up.sql) based on DATA_TABLE.md
- Add seed data ingestion tool (cmd/seed) and static TypeScript content exporter (cmd/export-content)
- Add configuration loader (internal/config) supporting environment variables and .env files (HOST, PORT, DB_PATH, GIN_MODE, CORS_ALLOW_ORIGINS, AUTO_MIGRATE)
- Add lightweight multi-stage Dockerfile and docker-compose.yml with persistent SQLite volume
- Add comprehensive architecture specification (ARCHITECTURE.md) and REST API interface specification (API_INTERFACE.md)
- Harmonize frontend publications page to consume isHighlight flag from database-synced content
2026-08-24 17:34:51 +09:00

447 lines
15 KiB
Go

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")
}