package fetchguard import ( "errors" "io" "net" "net/http" "net/http/httptest" "testing" "time" ) // Gap (f): PublicOnlyMode private-IP rejection is already asserted by // TestFetchPrivateIPBlockedInBothModes/PublicOnlyMode. This file adds // PublicOnlyMode accepting any host when the dial-time IP check is // satisfied (loopback httptest with skipReservedCheck — a live public // IP dial would require outbound network and is not asserted here). func TestFetchPublicOnlyModeAcceptsAnyHostWhenPublic(t *testing.T) { srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") io.WriteString(w, "ok") })) t.Cleanup(srv.Close) res, err := Fetch(t.Context(), srv.URL, withTestLoopback(srv, Policy{ Mode: PublicOnlyMode, MaxBytes: 1024, Timeout: 2 * time.Second, }), nil) if err != nil { t.Fatalf("PublicOnlyMode must accept the httptest host: %v", err) } if string(res.Body) != "ok" { t.Fatalf("body = %q", res.Body) } } func TestFetchPublicOnlyModePrivateIPRejected(t *testing.T) { // Explicit restatement of the private-IP case under PublicOnlyMode so // this coverage file names both halves of gap (f). srv := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Error("handler must not run for a private dial") })) t.Cleanup(srv.Close) _, err := Fetch(t.Context(), srv.URL, Policy{ Mode: PublicOnlyMode, Timeout: 2 * time.Second, MaxBytes: 1024, }, nil) if reasonFrom(t, err) != ReasonPrivateIP { t.Fatalf("reason = %q, want %s", reasonFrom(t, err), ReasonPrivateIP) } } func TestHostAllowedExactAndDottedSuffix(t *testing.T) { allowed := []string{"discogs.com", ""} if !hostAllowed("discogs.com", allowed) { t.Fatal("exact match") } if !hostAllowed("api.discogs.com", allowed) { t.Fatal("dotted-suffix match") } if hostAllowed("evil-discogs.com", allowed) { t.Fatal("raw suffix must not match") } if hostAllowed("example.test", allowed) { t.Fatal("unrelated host") } } func TestMapTransportErrorReasons(t *testing.T) { if got := mapTransportError(errPrivateIP); got.Reason != ReasonPrivateIP { t.Fatalf("private_ip = %s", got.Reason) } dns := &net.DNSError{Err: "no such host", Name: "nope.test", IsNotFound: true} if got := mapTransportError(dns); got.Reason != ReasonUnresolvable { t.Fatalf("dns = %s", got.Reason) } if got := mapTransportError(errors.New("connection reset")); got.Reason != ReasonNetworkError { t.Fatalf("other = %s", got.Reason) } } func TestErrorStringWithoutInner(t *testing.T) { e := &Error{Reason: ReasonScheme} if e.Error() != "fetchguard: scheme" { t.Fatalf("Error() = %q", e.Error()) } var nilE *Error if nilE.Error() != "fetchguard: error" { t.Fatalf("nil Error() = %q", nilE.Error()) } if nilE.Unwrap() != nil { t.Fatal("nil Unwrap") } }