diff --git a/LICENSE.txt b/LICENSE.txt index 76c96c2..52ba238 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2019 Roy Rosenzweig Center for History and New Media +Copyright 2019-2026 Roy Rosenzweig Center for History and New Media Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/internal/datasets/apb/reference.go b/internal/datasets/apb/reference.go new file mode 100644 index 0000000..0d81581 --- /dev/null +++ b/internal/datasets/apb/reference.go @@ -0,0 +1,15 @@ +package apb + +import "net/http" + +const singleReferenceError = "400 Bad request. Please provide exactly one reference." + +func requireSingleReference(w http.ResponseWriter, r *http.Request) (string, bool) { + references := r.URL.Query()["ref"] + if len(references) != 1 { + http.Error(w, singleReferenceError, http.StatusBadRequest) + return "", false + } + + return references[0], true +} diff --git a/internal/datasets/apb/reference_test.go b/internal/datasets/apb/reference_test.go new file mode 100644 index 0000000..2515147 --- /dev/null +++ b/internal/datasets/apb/reference_test.go @@ -0,0 +1,46 @@ +package apb + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestVerseHandlersRequireExactlyOneReference(t *testing.T) { + handler := New(nil) + handlers := []struct { + name string + path string + handler http.HandlerFunc + }{ + {name: "verse", path: "/apb/verse", handler: handler.APBVerseHandler()}, + {name: "verse quotations", path: "/apb/verse-quotations", handler: handler.APBVerseQuotationsHandler()}, + {name: "verse trend", path: "/apb/verse-trend", handler: handler.APBVerseTrendHandler()}, + } + queries := []struct { + name string + query string + }{ + {name: "missing", query: ""}, + {name: "repeated", query: "?ref=Genesis+1%3A1&ref=John+1%3A1"}, + } + + for _, endpoint := range handlers { + for _, query := range queries { + t.Run(endpoint.name+"/"+query.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, endpoint.path+query.query, nil) + response := httptest.NewRecorder() + + endpoint.handler.ServeHTTP(response, request) + + if response.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want %d", response.Code, http.StatusBadRequest) + } + if body := strings.TrimSpace(response.Body.String()); body != singleReferenceError { + t.Fatalf("body = %q, want %q", body, singleReferenceError) + } + }) + } + } +} diff --git a/internal/datasets/apb/verse.go b/internal/datasets/apb/verse.go index 739b23f..d64a4fb 100644 --- a/internal/datasets/apb/verse.go +++ b/internal/datasets/apb/verse.go @@ -38,12 +38,14 @@ func (h *Handler) APBVerseHandler() http.HandlerFunc { ` return func(w http.ResponseWriter, r *http.Request) { - - refs := r.URL.Query()["ref"] + ref, ok := requireSingleReference(w, r) + if !ok { + return + } var result Verse - err := h.db.QueryRow(r.Context(), verseQuery, refs[0]).Scan(&result.Reference, &result.Text) + err := h.db.QueryRow(r.Context(), verseQuery, ref).Scan(&result.Reference, &result.Text) if errors.Is(err, pgx.ErrNoRows) { http.Error(w, "404 Not found.", http.StatusNotFound) return @@ -53,7 +55,7 @@ func (h *Handler) APBVerseHandler() http.HandlerFunc { } related := make([]string, 0) - rows, err := h.db.Query(r.Context(), relatedVerseQuery, refs[0]) + rows, err := h.db.Query(r.Context(), relatedVerseQuery, ref) if err != nil { internalServerError(w, "error querying related verses", err) return diff --git a/internal/datasets/apb/verse_quotations.go b/internal/datasets/apb/verse_quotations.go index 7138bc7..8590146 100644 --- a/internal/datasets/apb/verse_quotations.go +++ b/internal/datasets/apb/verse_quotations.go @@ -29,13 +29,15 @@ func (h *Handler) APBVerseQuotationsHandler() http.HandlerFunc { ` return func(w http.ResponseWriter, r *http.Request) { - - refs := r.URL.Query()["ref"] + ref, ok := requireSingleReference(w, r) + if !ok { + return + } results := make([]VerseQuotation, 0) var row VerseQuotation - rows, err := h.db.Query(r.Context(), query, refs[0]) + rows, err := h.db.Query(r.Context(), query, ref) if err != nil { internalServerError(w, "error querying verse quotations", err) return diff --git a/internal/datasets/apb/verse_trend.go b/internal/datasets/apb/verse_trend.go index a285626..6d05b0c 100644 --- a/internal/datasets/apb/verse_trend.go +++ b/internal/datasets/apb/verse_trend.go @@ -44,14 +44,8 @@ func (h *Handler) APBVerseTrendHandler() http.HandlerFunc { ` return func(w http.ResponseWriter, r *http.Request) { - - // Return a 404 error if we don't get exactly one reference - queryRef := r.URL.Query()["ref"] - var ref string - if len(queryRef) == 1 { - ref = queryRef[0] - } else { - http.Error(w, "400 Bad request. Please provide exactly one reference.", http.StatusBadRequest) + ref, ok := requireSingleReference(w, r) + if !ok { return } diff --git a/internal/datasets/apb/verse_trend_test.go b/internal/datasets/apb/verse_trend_test.go index eadff33..5e00548 100644 --- a/internal/datasets/apb/verse_trend_test.go +++ b/internal/datasets/apb/verse_trend_test.go @@ -13,16 +13,6 @@ func TestVerseTrendRejectsInvalidParameters(t *testing.T) { path string body string }{ - { - name: "missing reference", - path: "/apb/verse-trend", - body: "400 Bad request. Please provide exactly one reference.", - }, - { - name: "multiple references", - path: "/apb/verse-trend?ref=Gen.1.1&ref=John.1.1", - body: "400 Bad request. Please provide exactly one reference.", - }, { name: "invalid corpus", path: "/apb/verse-trend?ref=Gen.1.1&corpus=unknown",