check / check (push) Waiting to run
The new package internal/providers builds the cache, the fetcher, the processor, the image service, the session manager and the encrypted URL generator from the config. pixad's fx graph provides them with providers.Constructors(), and handlers.New receives them built. The server tests build the same graph, with only the fetcher's dial function replaced. database.New opens the database, so these parts can read it while fx builds them; the cache's stop hook still runs before the database's. Each setting's default is declared in internal/config, and internal/providers works out the default cache_max_bytes. The cache's MaxBytes is the one value for the disk cache limit, and 0 turns the disk cache off. The fetcher and the processor take explicit values. Model: opus-5-5
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
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)
|
|
}
|
|
}
|