aboutsummaryrefslogtreecommitdiff
path: root/server_test.go
diff options
context:
space:
mode:
authorSam Scholten2026-06-14 20:00:15 +1000
committerSam Scholten2026-06-14 20:00:15 +1000
commitdecc46c876e7b5552f5f5ecac4ee4f1a64ad1d62 (patch)
tree46875e236a062189115c0cd8ed8f1d82980c16b7 /server_test.go
downloadabvjt-decc46c876e7b5552f5f5ecac4ee4f1a64ad1d62.tar.gz
abvjt-decc46c876e7b5552f5f5ecac4ee4f1a64ad1d62.zip
Initial implementation: scrape, serve, UI, container, deploymentHEADmain
Diffstat (limited to 'server_test.go')
-rw-r--r--server_test.go406
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")
+ }
+}