diff options
| author | Sam Scholten | 2026-06-14 20:00:15 +1000 |
|---|---|---|
| committer | Sam Scholten | 2026-06-14 20:00:15 +1000 |
| commit | decc46c876e7b5552f5f5ecac4ee4f1a64ad1d62 (patch) | |
| tree | 46875e236a062189115c0cd8ed8f1d82980c16b7 /server_test.go | |
| download | abvjt-main.tar.gz abvjt-main.zip | |
Diffstat (limited to 'server_test.go')
| -rw-r--r-- | server_test.go | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..2dfd1fc --- /dev/null +++ b/server_test.go @@ -0,0 +1,406 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "path/filepath" + "strings" + "testing" +) + +func setupTestServer(t *testing.T) (*DB, *httptest.Server) { + t.Helper() + + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + + db, err := OpenDB(dbPath) + if err != nil { + t.Fatalf("OpenDB failed: %v", err) + } + + journals := []Journal{ + {FullName: "Journal of Applied Physics", Abbreviation: "J APPL PHYS"}, + {FullName: "Nature Medicine", Abbreviation: "NAT MED"}, + {FullName: "Physical Review Letters", Abbreviation: "PHYS REV LETT"}, + {FullName: "Nature Biotechnology", Abbreviation: "NAT BIOTECHNOL"}, + {FullName: "Nature & Science", Abbreviation: "NAT SCI"}, + {FullName: "C++ Weekly", Abbreviation: "C WEEKLY"}, + {FullName: "Physical Review (Letters)", Abbreviation: "PHYS REV LETT"}, + {FullName: "Proceedings of the National Academy of Sciences", Abbreviation: "PROC NAT ACAD SCI"}, + } + + if err := db.InsertJournals(journals); err != nil { + t.Fatalf("InsertJournals failed: %v", err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/", handleRoot()) + mux.HandleFunc("/api/search", handleSearch(db, 1000)) + mux.HandleFunc("/api/health", handleHealth(db, 1000)) + + server := httptest.NewServer(mux) + t.Cleanup(func() { + server.Close() + db.Close() + }) + + return db, server +} + +func get(t *testing.T, url string) *http.Response { + t.Helper() + resp, err := http.Get(url) + if err != nil { + t.Fatalf("GET %s failed: %v", url, err) + } + return resp +} + +func readBody(t *testing.T, resp *http.Response) string { + t.Helper() + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading body: %v", err) + } + return string(b) +} + +func decodeJournals(t *testing.T, body string) []Journal { + t.Helper() + var results []Journal + if err := json.Unmarshal([]byte(body), &results); err != nil { + t.Fatalf("json decode failed: %v\nbody: %s", err, body) + } + return results +} + +func TestGetRoot(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/") + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("expected 200, got %d", resp.StatusCode) + } + + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/html") { + t.Errorf("expected text/html content type, got %s", ct) + } + + body := readBody(t, resp) + if !strings.Contains(body, "Journal Abbreviations") { + t.Error("expected page title in HTML body") + } +} + +func TestGetRootNotFound(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/nonexistent") + defer resp.Body.Close() + + if resp.StatusCode != 404 { + t.Errorf("expected 404 for /nonexistent, got %d", resp.StatusCode) + } +} + +func TestSearchByFullName(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=physics") + body := readBody(t, resp) + + results := decodeJournals(t, body) + + found := false + for _, j := range results { + if j.FullName == "Journal of Applied Physics" { + found = true + if j.Abbreviation != "J APPL PHYS" { + t.Errorf("expected abbreviation 'J APPL PHYS', got %q", j.Abbreviation) + } + } + } + if !found { + t.Error("expected to find 'Journal of Applied Physics' when searching 'physics'") + } +} + +func TestSearchByAbbreviation(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=NAT+MED") + body := readBody(t, resp) + + results := decodeJournals(t, body) + + found := false + for _, j := range results { + if j.FullName == "Nature Medicine" { + found = true + } + } + if !found { + t.Errorf("expected to find 'Nature Medicine' when searching abbreviation 'NAT MED', got %d results", len(results)) + } +} + +func TestSearchCaseInsensitive(t *testing.T) { + _, server := setupTestServer(t) + + lowerResp := get(t, server.URL+"/api/search?q=nature+medicine") + lowerBody := readBody(t, lowerResp) + lowerResults := decodeJournals(t, lowerBody) + + upperResp := get(t, server.URL+"/api/search?q=NATURE+MEDICINE") + upperBody := readBody(t, upperResp) + upperResults := decodeJournals(t, upperBody) + + if len(lowerResults) == 0 { + t.Error("lowercase search returned no results") + } + if len(upperResults) == 0 { + t.Error("uppercase search returned no results") + } + if len(lowerResults) != len(upperResults) { + t.Errorf("case insensitive: lower=%d results, upper=%d results", len(lowerResults), len(upperResults)) + } +} + +func TestSearchMultipleMatches(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=nature") + body := readBody(t, resp) + + results := decodeJournals(t, body) + + if len(results) < 2 { + t.Errorf("expected at least 2 results for 'nature', got %d", len(results)) + } + + names := map[string]bool{} + for _, j := range results { + names[j.FullName] = true + } + if !names["Nature Medicine"] { + t.Error("expected 'Nature Medicine' in results") + } + if !names["Nature Biotechnology"] { + t.Error("expected 'Nature Biotechnology' in results") + } +} + +func TestSearchNoResults(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=zzzzznonexistent") + body := readBody(t, resp) + + if resp.StatusCode != 200 { + t.Errorf("expected 200 for no-results query, got %d", resp.StatusCode) + } + + results := decodeJournals(t, body) + if len(results) != 0 { + t.Errorf("expected 0 results for nonexistent query, got %d", len(results)) + } +} + +func TestSearchMissingQuery(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search") + body := readBody(t, resp) + + if resp.StatusCode != 400 { + t.Errorf("expected 400 for missing q param, got %d", resp.StatusCode) + } + + if !strings.Contains(body, "missing query") { + t.Errorf("expected error message about missing query, got: %s", body) + } +} + +func TestSearchWrongMethod(t *testing.T) { + _, server := setupTestServer(t) + + resp, err := http.Post(server.URL+"/api/search?q=test", "text/plain", nil) + if err != nil { + t.Fatalf("POST request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 405 { + t.Errorf("expected 405 for POST, got %d", resp.StatusCode) + } +} + +func TestSearchLimitParameter(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=nature&limit=1") + body := readBody(t, resp) + + results := decodeJournals(t, body) + if len(results) > 1 { + t.Errorf("expected at most 1 result with limit=1, got %d", len(results)) + } +} + +func TestSearchInvalidLimit(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=nature&limit=abc") + body := readBody(t, resp) + + if resp.StatusCode != 200 { + t.Errorf("expected 200 with invalid limit (should default), got %d", resp.StatusCode) + } + + results := decodeJournals(t, body) + if len(results) == 0 { + t.Error("invalid limit should default to 50 and still return results") + } +} + +func TestSearchSpecialChars(t *testing.T) { + _, server := setupTestServer(t) + + for _, q := range []string{"Nature & Science", "C++"} { + resp := get(t, server.URL+"/api/search?q="+url.QueryEscape(q)) + body := readBody(t, resp) + + if resp.StatusCode != 200 { + t.Errorf("search for %q returned %d", q, resp.StatusCode) + } + + results := decodeJournals(t, body) + if len(results) == 0 { + t.Errorf("expected results for %q", q) + } + } +} + +func TestHealthEndpoint(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/health") + body := readBody(t, resp) + + if resp.StatusCode != 200 { + t.Errorf("expected 200, got %d", resp.StatusCode) + } + + var result map[string]interface{} + if err := json.Unmarshal([]byte(body), &result); err != nil { + t.Fatalf("json decode failed: %v", err) + } + + if result["status"] != "ok" { + t.Errorf("expected status 'ok', got %v", result["status"]) + } + if result["db_loaded"] != true { + t.Errorf("expected db_loaded true, got %v", result["db_loaded"]) + } + count, ok := result["total_journals"].(float64) + if !ok || count < 1 { + t.Errorf("expected total_journals >= 1, got %v", result["total_journals"]) + } +} + +func TestHealthWrongMethod(t *testing.T) { + _, server := setupTestServer(t) + + resp, err := http.Post(server.URL+"/api/health", "text/plain", nil) + if err != nil { + t.Fatalf("POST request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 405 { + t.Errorf("expected 405 for POST to health, got %d", resp.StatusCode) + } +} + +func TestSearchResponseIsJSONArray(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=nature") + body := readBody(t, resp) + + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "application/json") { + t.Errorf("expected application/json, got %s", ct) + } + + if !strings.HasPrefix(strings.TrimSpace(body), "[") { + t.Errorf("expected JSON array response, got: %s", body[:min(len(body), 50)]) + } +} + +func TestSearchResultStructure(t *testing.T) { + _, server := setupTestServer(t) + + resp := get(t, server.URL+"/api/search?q=Nature+Medicine") + body := readBody(t, resp) + + results := decodeJournals(t, body) + if len(results) == 0 { + t.Fatal("expected at least one result") + } + + j := results[0] + if j.ID == 0 { + t.Error("expected non-zero ID") + } + if j.FullName == "" { + t.Error("expected non-empty FullName") + } + if j.Abbreviation == "" { + t.Error("expected non-empty Abbreviation") + } +} + +func TestRateLimiting(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + db, err := OpenDB(dbPath) + if err != nil { + t.Fatalf("OpenDB failed: %v", err) + } + journals := []Journal{ + {FullName: "Test Journal", Abbreviation: "TEST J"}, + } + if err := db.InsertJournals(journals); err != nil { + t.Fatalf("InsertJournals failed: %v", err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/api/search", handleSearch(db, 3)) + srv := httptest.NewServer(mux) + t.Cleanup(func() { + srv.Close() + db.Close() + }) + + tooManyCount := 0 + for i := 0; i < 10; i++ { + resp := get(t, srv.URL+"/api/search?q=test") + resp.Body.Close() + if resp.StatusCode == 429 { + tooManyCount++ + } + } + + if tooManyCount == 0 { + t.Error("expected at least one 429 response when rate limit is 3/min and we made 10 rapid requests") + } +} |
