package providers import ( "encoding/json" "net/http" "net/http/httptest" "net/netip" "path/filepath" "testing" "time" "github.com/go-chi/chi/v5" "github.com/prometheus/client_golang/prometheus" "go.uber.org/fx" "go.uber.org/fx/fxtest" "sneak.berlin/go/pixa/internal/config" "sneak.berlin/go/pixa/internal/database" "sneak.berlin/go/pixa/internal/globals" "sneak.berlin/go/pixa/internal/handlers" "sneak.berlin/go/pixa/internal/healthcheck" "sneak.berlin/go/pixa/internal/logger" ) // TestFetcherFromConfigRefusesBlockedNetworks builds the handlers as pixad // does, in an fx app with the providers of pixad's fx app, and requests an // image from 192.0.2.10, which is on the allowlist and in blocked_networks. // The URL check accepts that address; only the dialer that refuses internal // addresses checks blocked_networks, so the answer is 403 only if the // fetcher this package builds from the config connects with that dialer. // Any other dialer would try to connect until the upstream fetch timeout, // which is short so that the test then fails quickly. func TestFetcherFromConfigRefusesBlockedNetworks(t *testing.T) { t.Parallel() const host = "192.0.2.10" stateDir := t.TempDir() cfg := &config.Config{ SigningKey: testSigningKey, StateDir: stateDir, DBURL: "file:" + filepath.Join(stateDir, "state.sqlite3"), AllowlistHosts: []string{host}, BlockedNetworks: []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}, UpstreamFetchTimeout: 2 * time.Second, // With no connection slots, the fetch would fail before dialing. UpstreamConnectionsPerHost: config.DefaultUpstreamConnectionsPerHost, UpstreamConnections: config.DefaultUpstreamConnections, UpstreamMaxResponseSize: config.DefaultUpstreamMaxResponseSize, MaxConcurrentProcessing: 1, } var h *handlers.Handlers app := fxtest.New(t, fx.Supply(cfg), fx.Provide(globals.New, logger.New, database.New, healthcheck.New, handlers.New, func() prometheus.Registerer { return prometheus.NewRegistry() }), fx.Provide(Constructors()...), fx.Populate(&h), ) app.RequireStart() t.Cleanup(app.RequireStop) r := chi.NewRouter() r.Get("/v1/image/*", h.HandleImage()) target := "/v1/image/" + host + "/images/photo.jpg/50x50.jpeg" rec := httptest.NewRecorder() r.ServeHTTP(rec, httptest.NewRequestWithContext( t.Context(), http.MethodGet, target, nil)) t.Logf("GET %s: %d %s", target, rec.Code, rec.Body) if rec.Code != http.StatusForbidden { t.Errorf("status = %d, want %d", rec.Code, http.StatusForbidden) } if ct := rec.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("Content-Type = %q, want application/json", ct) } var body struct { Error string `json:"error"` Status int `json:"status"` Timestamp string `json:"timestamp"` } err := json.NewDecoder(rec.Body).Decode(&body) if err != nil { t.Fatalf("decoding response body: %v", err) } if body.Error != "forbidden" || body.Status != http.StatusForbidden { t.Errorf("body error = %q, status = %d; want %q, %d", body.Error, body.Status, "forbidden", http.StatusForbidden) } _, err = time.Parse(time.RFC3339, body.Timestamp) if err != nil { t.Errorf("body timestamp %q is not RFC 3339: %v", body.Timestamp, err) } }