Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractAPIKeyBearerAndHeader(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
|
||||
r.Header.Set("Authorization", "Bearer dk_abc")
|
||||
if got := extractAPIKey(r); got != "dk_abc" {
|
||||
t.Fatalf("bearer: got %q", got)
|
||||
}
|
||||
|
||||
r2 := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
|
||||
r2.Header.Set("X-API-Key", "dk_xyz")
|
||||
if got := extractAPIKey(r2); got != "dk_xyz" {
|
||||
t.Fatalf("x-api-key: got %q", got)
|
||||
}
|
||||
|
||||
r3 := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
|
||||
r3.Header.Set("X-API-Key", "dk_header")
|
||||
r3.Header.Set("Authorization", "Bearer dk_bearer")
|
||||
if got := extractAPIKey(r3); got != "dk_bearer" {
|
||||
t.Fatalf("bearer should win (legacy): got %q", got)
|
||||
}
|
||||
|
||||
r3b := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
|
||||
r3b.Header.Set("X-Api-Key", "dk_legacy_spelling")
|
||||
if got := extractAPIKey(r3b); got != "dk_legacy_spelling" {
|
||||
t.Fatalf("X-Api-Key: got %q", got)
|
||||
}
|
||||
|
||||
r4 := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
|
||||
if got := extractAPIKey(r4); got != "" {
|
||||
t.Fatalf("missing key: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLimitOffset(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/x?limit=10&offset=5", nil)
|
||||
limit, offset := ParseLimitOffset(r)
|
||||
if limit != 10 || offset != 5 {
|
||||
t.Fatalf("got limit=%d offset=%d", limit, offset)
|
||||
}
|
||||
|
||||
r2 := httptest.NewRequest(http.MethodGet, "/x?limit=999&offset=-1", nil)
|
||||
limit, offset = ParseLimitOffset(r2)
|
||||
if limit != maxPageLimit || offset != 0 {
|
||||
t.Fatalf("caps: got limit=%d offset=%d want max=%d", limit, offset, maxPageLimit)
|
||||
}
|
||||
|
||||
// Invalid / missing params are silently normalized (not 400).
|
||||
r3 := httptest.NewRequest(http.MethodGet, "/x?limit=abc&offset=xyz", nil)
|
||||
limit, offset = ParseLimitOffset(r3)
|
||||
if limit != defaultPageLimit || offset != 0 {
|
||||
t.Fatalf("invalid normalize: got limit=%d offset=%d", limit, offset)
|
||||
}
|
||||
|
||||
r4 := httptest.NewRequest(http.MethodGet, "/x", nil)
|
||||
limit, offset = ParseLimitOffset(r4)
|
||||
if limit != defaultPageLimit || offset != 0 {
|
||||
t.Fatalf("defaults: got limit=%d offset=%d", limit, offset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLimitOffsetMax(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/x?limit=1500", nil)
|
||||
limit, _ := ParseLimitOffsetMax(r, maxTreePageLimit)
|
||||
if limit != 1500 {
|
||||
t.Fatalf("got limit=%d want 1500", limit)
|
||||
}
|
||||
limit, _ = ParseLimitOffsetMax(r, maxPageLimit)
|
||||
if limit != maxPageLimit {
|
||||
t.Fatalf("got limit=%d want %d", limit, maxPageLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageSlice(t *testing.T) {
|
||||
items := []int{1, 2, 3, 4, 5}
|
||||
page, total := pageSlice(items, 2, 1)
|
||||
if total != 5 || len(page) != 2 || page[0] != 2 || page[1] != 3 {
|
||||
t.Fatalf("page=%v total=%d", page, total)
|
||||
}
|
||||
page, total = pageSlice(items, 10, 10)
|
||||
if total != 5 || len(page) != 0 {
|
||||
t.Fatalf("empty page expected, got %v total=%d", page, total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListCampaignsNilServicePreservesParsedLimit(t *testing.T) {
|
||||
s := &Server{}
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/campaigns?limit=7&offset=3", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.handleListCampaigns(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status %d", w.Code)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"limit":7`) || !strings.Contains(body, `"offset":3`) || !strings.Contains(body, `"total":0`) {
|
||||
t.Fatalf("unexpected body %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1OpenAPIDocumentsPublicAPIAuth(t *testing.T) {
|
||||
body := string(v1OpenAPIYAML)
|
||||
for _, want := range []string{
|
||||
"BearerAuth:",
|
||||
"ApiKeyAuth:",
|
||||
"name: X-API-Key",
|
||||
"## Authentication",
|
||||
"/settings?tab=api-keys",
|
||||
"https://descrybe.io/api/v1",
|
||||
"Use my API key",
|
||||
"30 requests per minute",
|
||||
"Retry-After",
|
||||
"rate limit exceeded",
|
||||
"code: unauthorized",
|
||||
"LegacyAPIError",
|
||||
"security: []",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("OpenAPI missing auth doc %q", want)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(body, "Forbidden:") {
|
||||
t.Fatal("OpenAPI missing Forbidden response component")
|
||||
}
|
||||
// Public probes must opt out of document-level API-key security.
|
||||
if !strings.Contains(body, "/health:") || !strings.Contains(body, "/openapi.yaml:") {
|
||||
t.Fatal("OpenAPI missing public health/openapi paths")
|
||||
}
|
||||
for _, heavy := range []string{
|
||||
"/products/process:",
|
||||
"/feeds/{id}/sync:",
|
||||
"/feeds/{id}/extract-schema:",
|
||||
"/feeds/{id}/sync-process-sample:",
|
||||
"/export-feeds/{id}/generate:",
|
||||
"/export-feeds/{id}/export-products:",
|
||||
"/process:",
|
||||
"/process/{id}/retry:",
|
||||
} {
|
||||
if !strings.Contains(body, heavy) {
|
||||
t.Fatalf("OpenAPI missing heavy path %q", heavy)
|
||||
}
|
||||
}
|
||||
// Heavy mutations document 429 via shared component.
|
||||
if strings.Count(body, `"429": { $ref: "#/components/responses/TooManyRequests" }`) < 6 {
|
||||
t.Fatalf("expected multiple TooManyRequests refs on heavy mutations, got %d",
|
||||
strings.Count(body, `"429": { $ref: "#/components/responses/TooManyRequests" }`))
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1OpenAPIRouteMounted(t *testing.T) {
|
||||
s := &Server{}
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.handleV1OpenAPI(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status %d", w.Code)
|
||||
}
|
||||
if body := w.Body.String(); len(body) < 20 || body[:8] != "openapi:" {
|
||||
t.Fatalf("unexpected body prefix %q", body[:min(20, len(body))])
|
||||
}
|
||||
if cc := w.Header().Get("Cache-Control"); !strings.Contains(cc, "max-age=") || !strings.Contains(cc, "stale-while-revalidate=") {
|
||||
t.Fatalf("unexpected Cache-Control %q", cc)
|
||||
}
|
||||
if vary := w.Header().Get("Vary"); !strings.Contains(vary, "Accept-Encoding") {
|
||||
t.Fatalf("unexpected Vary %q", vary)
|
||||
}
|
||||
etag := w.Header().Get("ETag")
|
||||
if etag == "" {
|
||||
t.Fatal("missing ETag")
|
||||
}
|
||||
r304 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
|
||||
r304.Header.Set("If-None-Match", etag)
|
||||
w304 := httptest.NewRecorder()
|
||||
s.handleV1OpenAPI(w304, r304)
|
||||
if w304.Code != http.StatusNotModified {
|
||||
t.Fatalf("If-None-Match status %d", w304.Code)
|
||||
}
|
||||
|
||||
rGzip := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
|
||||
rGzip.Header.Set("Accept-Encoding", "gzip")
|
||||
wGzip := httptest.NewRecorder()
|
||||
s.handleV1OpenAPI(wGzip, rGzip)
|
||||
if wGzip.Code != http.StatusOK {
|
||||
t.Fatalf("gzip status %d", wGzip.Code)
|
||||
}
|
||||
if wGzip.Header().Get("Content-Encoding") != "gzip" {
|
||||
t.Fatalf("expected Content-Encoding gzip, got %q", wGzip.Header().Get("Content-Encoding"))
|
||||
}
|
||||
if len(wGzip.Body.Bytes()) == 0 || wGzip.Body.Len() >= len(v1OpenAPIYAML) {
|
||||
t.Fatalf("gzip body should be non-empty and smaller than raw (%d vs %d)", wGzip.Body.Len(), len(v1OpenAPIYAML))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user