From 2a0c30f9532cd4316c6a051cea89b9356a165361 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 6 Mar 2026 17:00:38 +0300 Subject: [PATCH] server: integration tests for route reuse and upstream fallback Signed-off-by: NotAShelf Change-Id: I0d476da19941238b32c6adf87dac2d876a6a6964 --- internal/server/integration_test.go | 97 +++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 internal/server/integration_test.go diff --git a/internal/server/integration_test.go b/internal/server/integration_test.go new file mode 100644 index 0000000..b78e77a --- /dev/null +++ b/internal/server/integration_test.go @@ -0,0 +1,97 @@ +package server_test + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "notashelf.dev/ncro/internal/cache" + "notashelf.dev/ncro/internal/config" + "notashelf.dev/ncro/internal/prober" + "notashelf.dev/ncro/internal/router" + "notashelf.dev/ncro/internal/server" +) + +// Verifies that the second identical narinfo request uses the cached route. +func TestRouteReuseOnSecondRequest(t *testing.T) { + requestCount := 0 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, ".narinfo") { + requestCount++ + w.Header().Set("Content-Type", "text/x-nix-narinfo") + io.WriteString(w, "StorePath: /nix/store/test-pkg\nURL: nar/test.nar\n") + return + } + w.WriteHeader(404) + })) + defer upstream.Close() + + f, _ := os.CreateTemp("", "ncro-int-*.db") + f.Close() + defer os.Remove(f.Name()) + db, _ := cache.Open(f.Name(), 1000) + defer db.Close() + + p := prober.New(0.3) + p.RecordLatency(upstream.URL, 10) + r := router.New(db, p, time.Hour, 5*time.Second) + ts := httptest.NewServer(server.New(r, p, []config.UpstreamConfig{{URL: upstream.URL}})) + defer ts.Close() + + resp1, _ := http.Get(ts.URL + "/deadbeef00000000.narinfo") + io.Copy(io.Discard, resp1.Body) + resp1.Body.Close() + + resp2, _ := http.Get(ts.URL + "/deadbeef00000000.narinfo") + io.Copy(io.Discard, resp2.Body) + resp2.Body.Close() + + if resp1.StatusCode != 200 || resp2.StatusCode != 200 { + t.Errorf("expected 200/200, got %d/%d", resp1.StatusCode, resp2.StatusCode) + } +} + +// Verifies that when the best-seeded upstream returns 404, the fallback upstream is used. +func TestUpstreamFailoverFallback(t *testing.T) { + good := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/x-nix-narinfo") + io.WriteString(w, "StorePath: /nix/store/fallback-pkg\n") + })) + defer good.Close() + + bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + })) + defer bad.Close() + + f, _ := os.CreateTemp("", "ncro-fb-*.db") + f.Close() + defer os.Remove(f.Name()) + db, _ := cache.Open(f.Name(), 1000) + defer db.Close() + + p := prober.New(0.3) + p.RecordLatency(bad.URL, 1) // bad appears fastest + p.RecordLatency(good.URL, 50) + + r := router.New(db, p, time.Hour, 5*time.Second) + ts := httptest.NewServer(server.New(r, p, []config.UpstreamConfig{ + {URL: bad.URL}, + {URL: good.URL}, + })) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/cafebabe00000000.narinfo") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("expected 200 via fallback, got %d", resp.StatusCode) + } +}