Ctrl K
ComunidadGuías…

internal/charts/charts_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package charts

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"net/netip"
	"net/url"
	"slices"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

const indexBody = `
apiVersion: v1
entries:
  podinfo:
    - version: 6.14.0
    - version: 6.15.1
    - version: 6.16.0-rc.1
    - version: 6.9.0
  other:
    - version: 1.2.3
`

type interruptedReader struct {
	delivered bool
}

func (r *interruptedReader) Read(into []byte) (int, error) {
	if r.delivered {
		return 0, errors.New("repository response interrupted")
	}
	r.delivered = true
	return copy(into, "entries:\n  podinfo:"), nil
}

func cacheFor(t *testing.T, handler http.HandlerFunc) (*Cache, *httptest.Server) {
	t.Helper()
	ts := httptest.NewServer(handler)
	t.Cleanup(ts.Close)
	address := ts.Listener.Addr().String()
	ts.URL = "http://charts.example.com"
	return cacheThrough(t, ts.Client(), address), ts
}

func tlsCacheFor(t *testing.T, handler http.HandlerFunc) (*Cache, *httptest.Server) {
	t.Helper()
	ts := httptest.NewTLSServer(handler)
	t.Cleanup(ts.Close)
	address := ts.Listener.Addr().String()
	ts.URL = "https://registry.example.com"
	return cacheThrough(t, ts.Client(), address), ts
}

func cacheThrough(t *testing.T, client *http.Client, address string) *Cache {
	t.Helper()
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return []netip.Addr{netip.MustParseAddr("93.184.216.34")}, nil
	}
	dial := func(ctx context.Context, network, _ string) (net.Conn, error) {
		var dialer net.Dialer
		return dialer.DialContext(ctx, network, address)
	}
	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	return newCache(ctx, client, DefaultTTL, lookup, dial)
}

func indexHandler(hits *int) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !strings.HasSuffix(r.URL.Path, "/index.yaml") {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		*hits++
		_, _ = w.Write([]byte(indexBody))
	}
}

func TestResolveIndexPicksTheHighestStableVersion(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))

	found, err := cache.Resolve(context.Background(), Repo{URL: ts.URL}, "podinfo")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if newest(found, "podinfo") != "6.15.1" {
		t.Fatalf("podinfo = %q, want 6.15.1 (prerelease 6.16.0-rc.1 must be skipped)", newest(found, "podinfo"))
	}
	if newest(found, "other") != "1.2.3" {
		t.Fatalf("other = %q, want 1.2.3", newest(found, "other"))
	}
}

func newest(found map[string][]string, chart string) string {
	list := found[chart]
	if len(list) == 0 {
		return ""
	}
	return list[0]
}

func TestResolveIndexTrimsTrailingSlash(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))

	_, err := cache.Resolve(context.Background(), Repo{URL: ts.URL + "/"}, "podinfo")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if hits != 1 {
		t.Fatalf("hits = %d, want 1", hits)
	}
}

func TestWarmPopulatesEveryChartFromOneFetch(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))
	repo := Repo{URL: ts.URL}

	cache.Warm(repo, "podinfo")
	cache.Warm(repo, "other")
	cache.Wait()

	if cache.Latest(repo, "podinfo") != "6.15.1" {
		t.Fatalf("podinfo = %q", cache.Latest(repo, "podinfo"))
	}
	if cache.Latest(repo, "other") != "1.2.3" {
		t.Fatalf("other = %q", cache.Latest(repo, "other"))
	}
	if hits != 1 {
		t.Fatalf("index fetched %d times, want 1 for an http repo", hits)
	}
}

func TestWarmHonoursTheTTL(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))
	repo := Repo{URL: ts.URL}

	cache.Warm(repo, "podinfo")
	cache.Wait()
	cache.Warm(repo, "podinfo")
	cache.Wait()

	if hits != 1 {
		t.Fatalf("hits = %d, want 1 while inside the ttl", hits)
	}

	cache.now = func() time.Time { return time.Now().Add(2 * DefaultTTL) }
	cache.Warm(repo, "podinfo")
	cache.Wait()

	if hits != 2 {
		t.Fatalf("hits = %d, want 2 after the ttl expired", hits)
	}
}

func TestASeededIndexAnswersWithoutTheNetwork(t *testing.T) {
	now := time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC)
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return nil, errors.New("the network was used")
	}
	cache := newCache(t.Context(), nil, time.Hour, lookup, nil)
	cache.now = func() time.Time { return now }
	repo := Repo{URL: "https://charts.example.com"}
	body := indexBody + "\n  preview-only:\n    - version: 7.0.0-rc.1\n"

	err := cache.Seed(repo, strings.NewReader(body), now)
	if err != nil {
		t.Fatalf("seed: %v", err)
	}
	versions, versionsErr := cache.Versions(t.Context(), repo, "podinfo")
	if versionsErr != nil {
		t.Fatalf("versions: %v", versionsErr)
	}
	if strings.Join(versions, ",") != "6.15.1,6.14.0,6.9.0" {
		t.Fatalf("versions = %v, want the cached stable versions", versions)
	}
	hits, searchErr := cache.Search(t.Context(), repo, "pod", 10)
	if searchErr != nil {
		t.Fatalf("search: %v", searchErr)
	}
	if len(hits) != 1 || hits[0].Name != "podinfo" {
		t.Fatalf("hits = %+v, want podinfo from the cached catalog", hits)
	}
	if cache.Latest(repo, "preview-only") != "" {
		t.Fatal("a preview-only chart entered the stable cache")
	}
}

func TestABrokenSeedDoesNotChangeTheCache(t *testing.T) {
	cache := New(t.Context(), nil, time.Hour)
	repo := Repo{URL: "https://charts.example.com"}

	err := cache.Seed(repo, strings.NewReader("entries: [not closed"), time.Now())
	if err == nil {
		t.Fatal("a malformed cached index was accepted")
	}
	if cache.Latest(repo, "podinfo") != "" {
		t.Fatal("a malformed cached index populated the cache")
	}
}

func TestWarmIgnoresEmptyInputs(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))

	cache.Warm(Repo{URL: ""}, "podinfo")
	cache.Warm(Repo{URL: ts.URL}, "")
	cache.Wait()

	if hits != 0 {
		t.Fatalf("hits = %d, want 0", hits)
	}
}

func TestLatestIsEmptyBeforeAnyFetch(t *testing.T) {
	cache, ts := cacheFor(t, indexHandler(new(int)))

	if cache.Latest(Repo{URL: ts.URL}, "podinfo") != "" {
		t.Fatalf("expected an empty version before warming")
	}
}

func TestWarmSurvivesAFailedFetch(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	})
	repo := Repo{URL: ts.URL}

	cache.Warm(repo, "podinfo")
	cache.Wait()

	if cache.Latest(repo, "podinfo") != "" {
		t.Fatalf("expected no version after a failed fetch")
	}
}

func TestAFailedWarmDoesNotHideTheNextSuccessfulFetch(t *testing.T) {
	hits := 0
	now := time.Date(2026, 9, 3, 18, 0, 0, 0, time.UTC)
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		hits++
		if hits == 1 {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		_, _ = w.Write([]byte(indexBody))
	})
	cache.now = func() time.Time { return now }
	repo := Repo{URL: ts.URL}
	cache.Warm(repo, "podinfo")
	cache.Wait()
	now = now.Add(negativeTTL)

	versions, err := cache.Versions(t.Context(), repo, "podinfo")
	if err != nil {
		t.Fatalf("versions after the repository recovered: %v", err)
	}
	if len(versions) == 0 || versions[0] != "6.15.1" {
		t.Fatalf("versions after recovery = %v", versions)
	}
	if hits != 2 {
		t.Fatalf("repository fetched %d times, want the failed warm and the recovery", hits)
	}
}

func TestConcurrentIdenticalOCIMissesShareOneRegistryRequest(t *testing.T) {
	var hits atomic.Int32
	started := make(chan struct{})
	proceed := make(chan struct{})
	var once sync.Once
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		hits.Add(1)
		once.Do(func() { close(started) })
		<-proceed
		_, _ = w.Write([]byte(`{"tags":["1.2.3"]}`))
	})
	repo := ociRepo(ts)

	const callers = 16
	errs := make(chan error, callers)
	for range callers {
		go func() {
			_, err := cache.Versions(t.Context(), repo, "keycloak")
			errs <- err
		}()
	}
	<-started
	close(proceed)
	for range callers {
		if err := <-errs; err != nil {
			t.Fatalf("versions: %v", err)
		}
	}
	if hits.Load() != 1 {
		t.Fatalf("registry requests = %d, want 1", hits.Load())
	}
}

func TestRegistryFetchWaiterCanCancelWithoutStartingAnotherRequest(t *testing.T) {
	var hits atomic.Int32
	var once sync.Once
	started := make(chan struct{})
	proceed := make(chan struct{})
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		hits.Add(1)
		once.Do(func() { close(started) })
		<-proceed
		_, _ = w.Write([]byte(`{"tags":["1.2.3"]}`))
	})
	repo := ociRepo(ts)
	leader := make(chan error, 1)
	go func() {
		_, err := cache.Versions(t.Context(), repo, "keycloak")
		leader <- err
	}()
	<-started
	canceled, cancel := context.WithCancel(t.Context())
	cancel()

	_, err := cache.Versions(canceled, repo, "keycloak")
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("waiter error = %v, want context cancellation", err)
	}
	if hits.Load() != 1 {
		t.Fatalf("registry requests = %d, want 1", hits.Load())
	}
	close(proceed)
	if err := <-leader; err != nil {
		t.Fatalf("leader versions: %v", err)
	}
}

func TestFailedFetchesAreCachedBriefly(t *testing.T) {
	var hits atomic.Int32
	now := time.Date(2026, 9, 3, 18, 0, 0, 0, time.UTC)
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		hits.Add(1)
		w.WriteHeader(http.StatusBadGateway)
	})
	cache.now = func() time.Time { return now }
	repo := Repo{URL: ts.URL}

	for range 2 {
		_, err := cache.Versions(t.Context(), repo, "podinfo")
		if err == nil {
			t.Fatal("failed repository reported success")
		}
	}
	if hits.Load() != 1 {
		t.Fatalf("requests inside the negative ttl = %d, want 1", hits.Load())
	}
	now = now.Add(negativeTTL)
	_, err := cache.Versions(t.Context(), repo, "podinfo")
	if err == nil {
		t.Fatal("failed repository reported success after the negative ttl")
	}
	if hits.Load() != 2 {
		t.Fatalf("requests at the negative ttl boundary = %d, want 2", hits.Load())
	}
}

func TestCallerControlledOCICacheEntriesAreEvictedByCapacity(t *testing.T) {
	cache := New(t.Context(), nil, DefaultTTL)
	cache.capacity = 2
	now := time.Date(2026, 9, 3, 18, 0, 0, 0, time.UTC)
	cache.now = func() time.Time { return now }
	repo := Repo{URL: "oci://registry.example.com/team", OCI: true}

	for _, chart := range []string{"first", "second", "third"} {
		unit := fetchUnit(repo, chart)
		flight, leader, err := cache.begin(unit)
		if err != nil || !leader {
			t.Fatalf("begin %s: leader=%v error=%v", chart, leader, err)
		}
		found := listing{versions: map[string][]string{chart: {"1.0.0"}}}
		cache.complete(unit, repo, found, flight, nil)
		now = now.Add(time.Second)
	}

	if cache.Latest(repo, "first") != "" {
		t.Fatal("oldest oci entry survived the capacity boundary")
	}
	if cache.Latest(repo, "second") != "1.0.0" || cache.Latest(repo, "third") != "1.0.0" {
		t.Fatalf("newer entries were evicted: second=%q third=%q", cache.Latest(repo, "second"), cache.Latest(repo, "third"))
	}
	if len(cache.fetched) != 2 || len(cache.lists) != 2 {
		t.Fatalf("cache sizes = fetched %d, lists %d, want 2 each", len(cache.fetched), len(cache.lists))
	}
}

func TestResolveIndexRejectsMalformedYAML(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("entries: [oops"))
	})

	_, err := cache.Resolve(context.Background(), Repo{URL: ts.URL}, "podinfo")

	if err == nil {
		t.Fatalf("expected a parse error")
	}
}

const mergeBesideAMapKey = `
entries:
  podinfo:
    - version: 6.14.0
? {kind: map}
: key
<<: {}
`

const unhashableKeyError = "yaml: runtime error: hash of unhashable type map[string]interface {}"

func TestASeededIndexWithAMergeBesideAMapKeyIsRefused(t *testing.T) {
	now := time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC)
	cache := newCache(t.Context(), nil, time.Hour, nil, nil)
	cache.now = func() time.Time { return now }
	repo := Repo{URL: "https://charts.example.com"}

	err := cache.Seed(repo, strings.NewReader(mergeBesideAMapKey), now)

	if err == nil || err.Error() != unhashableKeyError {
		t.Fatalf("seed error = %v, want %q", err, unhashableKeyError)
	}
	if cache.Latest(repo, "podinfo") != "" {
		t.Fatalf("podinfo = %q, want nothing cached from a refused index", cache.Latest(repo, "podinfo"))
	}
}

func TestResolveRefusesAnIndexWithAMergeBesideAMapKey(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(mergeBesideAMapKey))
	})

	_, err := cache.Resolve(t.Context(), Repo{URL: ts.URL}, "podinfo")

	want := "parse " + ts.URL + "/index.yaml: " + unhashableKeyError
	if err == nil || err.Error() != want {
		t.Fatalf("error = %v, want %q", err, want)
	}
}

func TestWarmSurvivesAnIndexWithAMergeBesideAMapKey(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(mergeBesideAMapKey))
	})
	repo := Repo{URL: ts.URL}

	cache.Warm(repo, "podinfo")
	cache.Wait()

	if cache.Latest(repo, "podinfo") != "" {
		t.Fatalf("podinfo = %q, want nothing cached from a refused index", cache.Latest(repo, "podinfo"))
	}
}

func TestResolveRefusesALocalRepositoryPassedDirectly(t *testing.T) {
	cache := New(t.Context(), http.DefaultClient, DefaultTTL)

	_, err := cache.Resolve(t.Context(), Repo{URL: "http://127.0.0.1/charts"}, "podinfo")

	if err == nil || !strings.Contains(err.Error(), "public address") {
		t.Fatalf("error = %v, want the local repository refused", err)
	}
}

func TestResolveRequiresTheURLAndOCISettingToAgree(t *testing.T) {
	cache := New(t.Context(), http.DefaultClient, DefaultTTL)
	cases := []Repo{
		{URL: "https://charts.example.com", OCI: true},
		{URL: "oci://registry.example.com/team", OCI: false},
	}

	for _, repo := range cases {
		_, err := cache.Resolve(t.Context(), repo, "podinfo")

		if err == nil || !strings.Contains(err.Error(), "does not match its oci setting") {
			t.Fatalf("repo = %+v, error = %v", repo, err)
		}
	}
}

func ociHandler(t *testing.T, tokens *int) http.HandlerFunc {
	t.Helper()
	return func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/token" {
			*tokens++
			_, _ = w.Write([]byte(`{"token":"abc123"}`))
			return
		}
		if r.Header.Get("Authorization") != "Bearer abc123" {
			w.Header().Set("WWW-Authenticate",
				fmt.Sprintf(`Bearer realm="https://%s/token",service="registry",scope="repository:team/keycloak:pull"`, r.Host))
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		if r.URL.Path != "/v2/team/keycloak/tags/list" {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		_, _ = w.Write([]byte(`{"tags":["0.21.14","0.22.0","latest","sha256-abc.sig","0.23.0-rc1"]}`))
	}
}

func ociRepo(ts *httptest.Server) Repo {
	return Repo{URL: "oci://" + strings.TrimPrefix(ts.URL, "https://") + "/team", OCI: true}
}

func TestResolveOCIFollowsTheBearerChallenge(t *testing.T) {
	tokens := 0
	cache, ts := tlsCacheFor(t, ociHandler(t, &tokens))

	found, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if newest(found, "keycloak") != "0.22.0" {
		t.Fatalf("keycloak = %q, want 0.22.0", newest(found, "keycloak"))
	}
	if tokens != 1 {
		t.Fatalf("token requests = %d, want 1", tokens)
	}
}

func TestOCICachesPerChart(t *testing.T) {
	tokens := 0
	cache, ts := tlsCacheFor(t, ociHandler(t, &tokens))
	repo := ociRepo(ts)

	cache.Warm(repo, "keycloak")
	cache.Wait()

	if cache.Latest(repo, "keycloak") != "0.22.0" {
		t.Fatalf("keycloak = %q", cache.Latest(repo, "keycloak"))
	}
	if cache.Latest(repo, "other") != "" {
		t.Fatalf("an unrelated chart must not be populated by an oci fetch")
	}
}

func TestResolveOCIRejectsABadURL(t *testing.T) {
	cache, _ := tlsCacheFor(t, ociHandler(t, new(int)))

	_, err := cache.Resolve(context.Background(), Repo{URL: "oci://registry-only", OCI: true}, "keycloak")

	if err == nil {
		t.Fatalf("expected an error for an oci url without a path")
	}
}

func TestResolveOCIGivesUpWhenTheTokenIsRejected(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/token" {
			_, _ = w.Write([]byte(`{"token":"abc123"}`))
			return
		}
		w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token"`, r.Host))
		w.WriteHeader(http.StatusUnauthorized)
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatalf("expected the second 401 to surface rather than looping")
	}
}

func TestResolveOCIWithoutARealm(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("WWW-Authenticate", "Basic")
		w.WriteHeader(http.StatusUnauthorized)
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatalf("expected an error when the challenge has no bearer realm")
	}
}

func TestResolveOCITokenEndpointFailure(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/token" {
			w.WriteHeader(http.StatusForbidden)
			return
		}
		w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token"`, r.Host))
		w.WriteHeader(http.StatusUnauthorized)
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatalf("expected the token failure to surface")
	}
}

func TestResolveOCIReportsATokenConnectionThatBreaks(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/token" {
			hijacker, ok := w.(http.Hijacker)
			if !ok {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			conn, _, err := hijacker.Hijack()
			if err != nil {
				return
			}
			_ = conn.Close()
			return
		}
		w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token"`, r.Host))
		w.WriteHeader(http.StatusUnauthorized)
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatal("a broken token connection was reported as a successful registry read")
	}
}

func TestResolveOCIAcceptsAccessToken(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/token" {
			_, _ = w.Write([]byte(`{"access_token":"xyz"}`))
			return
		}
		if r.Header.Get("Authorization") != "Bearer xyz" {
			w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token"`, r.Host))
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		_, _ = w.Write([]byte(`{"tags":["1.0.0"]}`))
	})

	found, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if newest(found, "keycloak") != "1.0.0" {
		t.Fatalf("keycloak = %q", newest(found, "keycloak"))
	}
}

func TestResolveOCIEmptyTokenResponse(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/token" {
			_, _ = w.Write([]byte(`{}`))
			return
		}
		w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token"`, r.Host))
		w.WriteHeader(http.StatusUnauthorized)
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatalf("expected an error for an empty token response")
	}
}

func TestResolveOCIRejectsMalformedTokenJSON(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/token" {
			_, _ = w.Write([]byte("{not json"))
			return
		}
		w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token"`, r.Host))
		w.WriteHeader(http.StatusUnauthorized)
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatalf("expected a token parse error")
	}
}

func TestResolveOCIRejectsMalformedTagJSON(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("{not json"))
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatalf("expected a tag parse error")
	}
}

func TestResolveOCIWithNoUsableTags(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{"tags":["latest","edge"]}`))
	})

	found, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if len(found) != 0 {
		t.Fatalf("found = %v, want empty", found)
	}
}

func TestResolveSurfacesNonAuthErrors(t *testing.T) {
	cache, ts := tlsCacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadGateway)
	})

	_, err := cache.Resolve(context.Background(), ociRepo(ts), "keycloak")

	if err == nil {
		t.Fatalf("expected a 502 to surface")
	}
}

func TestResolveRejectsAnUnusableURL(t *testing.T) {
	cache, _ := cacheFor(t, indexHandler(new(int)))

	_, err := cache.Resolve(context.Background(), Repo{URL: "://bad"}, "podinfo")

	if err == nil {
		t.Fatalf("expected a request build error")
	}
}

func internetFor(t *testing.T, handler http.HandlerFunc) *Cache {
	t.Helper()
	ts := httptest.NewTLSServer(handler)
	t.Cleanup(ts.Close)
	return cacheThrough(t, ts.Client(), ts.Listener.Addr().String())
}

func TestCheckRepoURLSortsFetchableFromNot(t *testing.T) {
	cases := []struct {
		name string
		url  string
		want bool
	}{
		{"an https repository", "https://charts.example.com/stable", true},
		{"a plain http repository", "http://charts.example.com/stable", true},
		{"an oci registry", "oci://registry.example.com/team", true},
		{"a public address", "https://93.184.216.34/charts", true},
		{"a file url", "file:///etc/passwd", false},
		{"a gopher url", "gopher://example.com/", false},
		{"no host at all", "https:///charts", false},
		{"cloud metadata", "http://169.254.169.254/latest/meta-data", false},
		{"loopback", "http://127.0.0.1:9090/charts", false},
		{"loopback by name", "http://localhost:9090/charts", false},
		{"a localhost subdomain", "http://spinoza.localhost/charts", false},
		{"localhost with a dns root", "http://localhost./charts", false},
		{"ipv6 loopback", "http://[::1]:9090/charts", false},
		{"an ipv4-mapped loopback", "http://[::ffff:127.0.0.1]/charts", false},
		{"a private range", "http://10.4.0.9/charts", false},
		{"carrier grade nat", "http://100.100.100.200/charts", false},
		{"a unique local address", "http://[fd00::1]/charts", false},
		{"the unspecified address", "http://0.0.0.0/charts", false},
		{"multicast", "http://224.0.0.1/charts", false},
		{"userinfo", "https://operator:[email protected]/charts", false},
		{"an encoded host", "https://%31%32%37.0.0.1/charts", false},
		{"unparseable", "://bad", false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := CheckRepoURL(tc.url)
			if (err == nil) != tc.want {
				t.Fatalf("CheckRepoURL(%q) = %v", tc.url, err)
			}
		})
	}
}

func TestARepositoryConnectionUsesTheAddressThatWasChecked(t *testing.T) {
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return []netip.Addr{netip.MustParseAddr("93.184.216.34")}, nil
	}
	connected := ""
	dial := func(_ context.Context, _, address string) (net.Conn, error) {
		connected = address
		return nil, errors.New("test dial stopped")
	}

	_, _ = publicDial(lookup, dial)(t.Context(), "tcp", "charts.example.com:443")

	if connected != "93.184.216.34:443" {
		t.Fatalf("dialed %q, want the checked address rather than the hostname", connected)
	}
}

func TestARepositoryConnectionNeedsAHostAndPort(t *testing.T) {
	lookedUp := false
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		lookedUp = true
		return nil, nil
	}
	dial := func(context.Context, string, string) (net.Conn, error) {
		return nil, errors.New("unexpected dial")
	}

	_, err := publicDial(lookup, dial)(t.Context(), "tcp", "charts.example.com")

	if err == nil || !strings.Contains(err.Error(), "repository address") {
		t.Fatalf("error = %v, want the malformed address named", err)
	}
	if lookedUp {
		t.Fatal("a malformed address reached DNS")
	}
}

func TestATLSRepositoryConnectionNeedsAHostAndPort(t *testing.T) {
	lookedUp := false
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		lookedUp = true
		return nil, nil
	}
	dial := func(context.Context, string, string) (net.Conn, error) {
		return nil, errors.New("unexpected dial")
	}

	_, err := publicTLSDial(defaultTransport(), lookup, dial)(t.Context(), "tcp", "charts.example.com")

	if err == nil || !strings.Contains(err.Error(), "repository address") {
		t.Fatalf("error = %v, want the malformed TLS address named", err)
	}
	if lookedUp {
		t.Fatal("a malformed TLS address reached DNS")
	}
}

func TestRepositoryDialRefusesLocalhostWithoutDNS(t *testing.T) {
	lookedUp := false
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		lookedUp = true
		return []netip.Addr{netip.MustParseAddr("93.184.216.34")}, nil
	}
	dial := func(context.Context, string, string) (net.Conn, error) {
		return nil, errors.New("unexpected dial")
	}

	_, err := publicDial(lookup, dial)(t.Context(), "tcp", "localhost:443")

	if err == nil || !strings.Contains(err.Error(), "this machine") {
		t.Fatalf("error = %v, want localhost refused", err)
	}
	if lookedUp {
		t.Fatal("localhost reached DNS")
	}
}

func TestARepositoryDNSFailureIsReported(t *testing.T) {
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return nil, errors.New("dns unavailable")
	}

	_, err := resolvePublic(t.Context(), lookup, "charts.example.com")

	if err == nil || !strings.Contains(err.Error(), "resolve repository host") {
		t.Fatalf("error = %v, want the DNS operation named", err)
	}
}

func TestARepositoryResolvingToNothingIsRefused(t *testing.T) {
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return nil, nil
	}

	_, err := resolvePublic(t.Context(), lookup, "charts.example.com")

	if err == nil || !strings.Contains(err.Error(), "resolved to no addresses") {
		t.Fatalf("error = %v, want the empty DNS answer named", err)
	}
}

func TestAMixedPublicAndPrivateDNSAnswerIsRefusedBeforeDialling(t *testing.T) {
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return []netip.Addr{
			netip.MustParseAddr("93.184.216.34"),
			netip.MustParseAddr("10.0.0.8"),
		}, nil
	}
	dialed := false
	dial := func(context.Context, string, string) (net.Conn, error) {
		dialed = true
		return nil, errors.New("test dial stopped")
	}

	_, err := publicDial(lookup, dial)(t.Context(), "tcp", "charts.example.com:443")

	if err == nil {
		t.Fatal("a mixed public and private dns answer was accepted")
	}
	if dialed {
		t.Fatal("an address was dialed before the complete dns answer was validated")
	}
}

func TestAReboundHostnameIsCheckedAgainForTheNextConnection(t *testing.T) {
	lookups := 0
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		lookups++
		if lookups == 1 {
			return []netip.Addr{netip.MustParseAddr("93.184.216.34")}, nil
		}
		return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
	}
	dials := 0
	dial := func(context.Context, string, string) (net.Conn, error) {
		dials++
		return nil, errors.New("test dial stopped")
	}
	guarded := publicDial(lookup, dial)

	_, _ = guarded(t.Context(), "tcp", "charts.example.com:443")
	_, err := guarded(t.Context(), "tcp", "charts.example.com:443")

	if err == nil || !strings.Contains(err.Error(), "non-public") {
		t.Fatalf("second connection error = %v, want the rebound address refused", err)
	}
	if lookups != 2 {
		t.Fatalf("dns lookups = %d, want one for every connection", lookups)
	}
	if dials != 1 {
		t.Fatalf("socket dials = %d, want only the first public answer dialed", dials)
	}
}

func TestAnIPv4MappedPrivateDNSAnswerIsRefused(t *testing.T) {
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return []netip.Addr{netip.MustParseAddr("::ffff:127.0.0.1")}, nil
	}
	dialed := false
	dial := func(context.Context, string, string) (net.Conn, error) {
		dialed = true
		return nil, errors.New("test dial stopped")
	}

	_, err := publicDial(lookup, dial)(t.Context(), "tcp", "charts.example.com:443")

	if err == nil {
		t.Fatal("an ipv4-mapped loopback answer was accepted")
	}
	if dialed {
		t.Fatal("the mapped loopback address was dialed")
	}
}

func TestTheGuardedTransportCannotBypassAddressValidation(t *testing.T) {
	transport := defaultTransport()
	transport.Proxy = http.ProxyFromEnvironment
	bypassedHTTP := false
	transport.DialContext = func(context.Context, string, string) (net.Conn, error) {
		bypassedHTTP = true
		return nil, errors.New("unguarded http dial")
	}
	bypassedTLS := false
	transport.DialTLSContext = func(context.Context, string, string) (net.Conn, error) {
		bypassedTLS = true
		return nil, errors.New("unguarded tls dial")
	}
	client := &http.Client{Transport: transport}
	lookup := func(context.Context, string, string) ([]netip.Addr, error) {
		return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
	}
	dial := func(context.Context, string, string) (net.Conn, error) {
		return nil, errors.New("guarded dial")
	}

	guarded := publicOnly(client, lookup, dial)
	configured, ok := guarded.Transport.(*http.Transport)
	if !ok {
		t.Fatalf("transport = %T", guarded.Transport)
	}
	if configured.Proxy != nil {
		t.Fatal("the guarded client can bypass validation through an http proxy")
	}
	_, httpErr := configured.DialContext(t.Context(), "tcp", "charts.example.com:80")
	if httpErr == nil || !strings.Contains(httpErr.Error(), "non-public") {
		t.Fatalf("http dial error = %v, want address validation", httpErr)
	}
	_, tlsErr := configured.DialTLSContext(t.Context(), "tcp", "charts.example.com:443")
	if tlsErr == nil || !strings.Contains(tlsErr.Error(), "non-public") {
		t.Fatalf("tls dial error = %v, want address validation", tlsErr)
	}
	if bypassedHTTP || bypassedTLS {
		t.Fatal("the guarded client used one of the caller's dialers")
	}
}

func TestTheGuardedTLSClientRefusesLegacyProtocols(t *testing.T) {
	for _, tc := range []struct {
		name       string
		configured *tls.Config
		want       uint16
	}{
		{name: "no caller configuration", want: tls.VersionTLS12},
		{name: "a legacy minimum", configured: &tls.Config{MinVersion: tls.VersionTLS10}, want: tls.VersionTLS12},
		{name: "a stricter minimum", configured: &tls.Config{MinVersion: tls.VersionTLS13}, want: tls.VersionTLS13},
	} {
		t.Run(tc.name, func(t *testing.T) {
			transport := &http.Transport{TLSClientConfig: tc.configured}

			got := tlsConfigFor(transport, "charts.example.com")

			if got.MinVersion != tc.want {
				t.Fatalf("minimum TLS version = %x, want %x", got.MinVersion, tc.want)
			}
			if got.ServerName != "charts.example.com" {
				t.Fatalf("server name = %q", got.ServerName)
			}
		})
	}
}

func TestARedirectOntoThisMachineIsRefused(t *testing.T) {
	cache := internetFor(t, func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "http://127.0.0.1:9/index.yaml", http.StatusFound)
	})

	_, err := cache.Resolve(context.Background(), Repo{URL: "https://charts.example.com"}, "podinfo")

	if err == nil {
		t.Fatal("a chart repository redirected the fetch onto loopback and it followed")
	}
}

func TestARedirectToAPublicHostIsFollowed(t *testing.T) {
	cache := internetFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/index.yaml" {
			http.Redirect(w, r, "https://example.com/mirror/index.yaml", http.StatusFound)
			return
		}
		_, _ = w.Write([]byte(indexBody))
	})

	found, err := cache.Resolve(context.Background(), Repo{URL: "https://example.com"}, "podinfo")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}

	if newest(found, "podinfo") != "6.15.1" {
		t.Fatalf("podinfo = %q", newest(found, "podinfo"))
	}
}

func TestAnEndlessRedirectChainIsCutOff(t *testing.T) {
	hops := 0
	cache := internetFor(t, func(w http.ResponseWriter, r *http.Request) {
		hops++
		http.Redirect(w, r, "https://example.com/again", http.StatusFound)
	})

	_, err := cache.Resolve(context.Background(), Repo{URL: "https://example.com"}, "podinfo")

	if err == nil {
		t.Fatal("expected the redirect chain to be cut off")
	}
	if hops > maxRedirects+1 {
		t.Fatalf("hops = %d, want the chain stopped at %d", hops, maxRedirects)
	}
}

func TestABearerRealmOnAnotherHostIsRefused(t *testing.T) {
	cache := internetFor(t, func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("WWW-Authenticate", `Bearer realm="http://169.254.169.254/latest/meta-data",service="registry"`)
		w.WriteHeader(http.StatusUnauthorized)
	})

	_, err := cache.Resolve(context.Background(), Repo{URL: "oci://registry.example.com/team", OCI: true}, "keycloak")

	if err == nil {
		t.Fatal("the registry pointed the token request at another host and it followed")
	}
}

func TestABearerRealmOnASiblingOfTheRegistryIsAllowed(t *testing.T) {
	cache := internetFor(t, func(w http.ResponseWriter, r *http.Request) {
		if r.Host == "auth.example.com" {
			_, _ = w.Write([]byte(`{"token":"abc123"}`))
			return
		}
		if r.Header.Get("Authorization") != "Bearer abc123" {
			w.Header().Set("WWW-Authenticate", `Bearer realm="https://auth.example.com/token",service="registry"`)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		_, _ = w.Write([]byte(`{"tags":["1.0.0"]}`))
	})

	found, err := cache.Resolve(context.Background(), Repo{URL: "oci://registry.example.com/team", OCI: true}, "keycloak")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}

	if newest(found, "keycloak") != "1.0.0" {
		t.Fatalf("keycloak = %q", newest(found, "keycloak"))
	}
}

func TestTheTokenRequestEscapesTheChallengeAndKeepsTheRealmQuery(t *testing.T) {
	asked := ""
	cache := internetFor(t, func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.URL.Path, "/token") {
			asked = r.URL.RawQuery
			_, _ = w.Write([]byte(`{"token":"abc123"}`))
			return
		}
		if r.Header.Get("Authorization") != "Bearer abc123" {
			w.Header().Set("WWW-Authenticate",
				`Bearer realm="https://example.com/token?tenant=acme",service="reg istry",scope="repository:team/keycloak:pull"`)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		_, _ = w.Write([]byte(`{"tags":["1.0.0"]}`))
	})

	_, err := cache.Resolve(context.Background(), Repo{URL: "oci://example.com/team", OCI: true}, "keycloak")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}

	query, parseErr := url.ParseQuery(asked)
	if parseErr != nil {
		t.Fatalf("parse %q: %v", asked, parseErr)
	}
	if query.Get("tenant") != "acme" {
		t.Fatalf("query = %q, want the realm's own parameters kept", asked)
	}
	if query.Get("service") != "reg istry" {
		t.Fatalf("service = %q, want it escaped rather than pasted in", query.Get("service"))
	}
	if query.Get("scope") != "repository:team/keycloak:pull" {
		t.Fatalf("scope = %q", query.Get("scope"))
	}
}

func TestTheChartNameCannotReshapeTheRegistryPath(t *testing.T) {
	asked := ""
	cache := internetFor(t, func(w http.ResponseWriter, r *http.Request) {
		asked = r.URL.EscapedPath()
		_, _ = w.Write([]byte(`{"tags":["1.0.0"]}`))
	})

	_, err := cache.Resolve(context.Background(), Repo{URL: "oci://example.com/team", OCI: true}, "../../secrets")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}

	if asked != "/v2/team/..%2F..%2Fsecrets/tags/list" {
		t.Fatalf("path = %q, want the chart name kept inside one segment", asked)
	}
}

func TestNewerComparesSemver(t *testing.T) {
	cases := []struct {
		current string
		latest  string
		want    bool
	}{
		{"6.14.0", "6.15.1", true},
		{"6.15.1", "6.15.1", false},
		{"6.16.0", "6.15.1", false},
		{"v1.21.0", "v1.22.0", true},
		{"1.0.0+86d6320df72b", "1.0.1", true},
		{"", "6.15.1", false},
		{"6.14.0", "", false},
		{"not-semver", "6.15.1", false},
		{"6.14.0", "not-semver", false},
	}
	for _, tc := range cases {
		if got := Newer(tc.current, tc.latest); got != tc.want {
			t.Fatalf("Newer(%q, %q) = %v, want %v", tc.current, tc.latest, got, tc.want)
		}
	}
}

func TestParseChallengeIgnoresJunk(t *testing.T) {
	got := parseChallenge(`Bearer realm="https://auth.example/token",service="reg",bogus`)

	if got["realm"] != "https://auth.example/token" {
		t.Fatalf("realm = %q", got["realm"])
	}
	if got["service"] != "reg" {
		t.Fatalf("service = %q", got["service"])
	}
}

func TestSortVersionsAcceptsAnOCIBuildMetadataTag(t *testing.T) {
	got := sortVersions([]string{"1.2.0", "1.3.0_20260101", "1.1.0"})

	want := []string{"1.3.0_20260101", "1.2.0", "1.1.0"}
	if !slices.Equal(got, want) {
		t.Fatalf("sorted = %v, want %v; Helm writes + as _ in an OCI tag, so that one was being dropped", got, want)
	}
}

func TestSortVersionsKeepsTheTagItWasGiven(t *testing.T) {
	got := sortVersions([]string{"2.0.0_abc"})

	if !slices.Equal(got, []string{"2.0.0_abc"}) {
		t.Fatalf("sorted = %v, want the tag as published rather than a rewritten one", got)
	}
}

func TestSortVersionsStillSkipsPrereleases(t *testing.T) {
	got := sortVersions([]string{"1.0.0", "2.0.0-rc.1"})

	if !slices.Equal(got, []string{"1.0.0"}) {
		t.Fatalf("sorted = %v, want a prerelease left out the way Helm leaves it out", got)
	}
}

func TestSortVersionsReportsNothingForAnAllPrereleaseRepo(t *testing.T) {
	got := sortVersions([]string{"2.0.0-rc.1", "2.0.0-rc.2"})

	if len(got) != 0 {
		t.Fatalf("sorted = %v, want none; suggesting an upgrade to a release candidate is worse than saying nothing", got)
	}
}

func TestSortVersionsSkipsATagThatIsNotSemverEvenAfterTranslation(t *testing.T) {
	got := sortVersions([]string{"1.0.0_build_2"})

	if len(got) != 0 {
		t.Fatalf("sorted = %v; underscores are not legal in build metadata, so this is not a version", got)
	}
}

func TestSortVersionsComparesTranslatedTagsByVersionNotText(t *testing.T) {
	got := sortVersions([]string{"1.10.0_20260101", "1.9.0_20260102"})

	want := []string{"1.10.0_20260101", "1.9.0_20260102"}
	if !slices.Equal(got, want) {
		t.Fatalf("sorted = %v, want the higher version first rather than the later build stamp", got)
	}
}

func TestVersionsReturnsTheSortedListAndCaches(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))
	repo := Repo{URL: ts.URL}

	got, err := cache.Versions(context.Background(), repo, "podinfo")
	if err != nil {
		t.Fatalf("versions: %v", err)
	}
	want := []string{"6.15.1", "6.14.0", "6.9.0"}
	if !slices.Equal(got, want) {
		t.Fatalf("versions = %v, want %v", got, want)
	}

	again, err := cache.Versions(context.Background(), repo, "podinfo")
	if err != nil {
		t.Fatalf("versions again: %v", err)
	}
	if !slices.Equal(again, want) {
		t.Fatalf("versions again = %v, want %v", again, want)
	}
	if hits != 1 {
		t.Fatalf("hits = %d, want the second read served from the cache", hits)
	}
}

func TestVersionsRefetchesAfterTheTTL(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))
	repo := Repo{URL: ts.URL}

	_, err := cache.Versions(context.Background(), repo, "podinfo")
	if err != nil {
		t.Fatalf("versions: %v", err)
	}
	cache.now = func() time.Time { return time.Now().Add(2 * DefaultTTL) }
	_, err = cache.Versions(context.Background(), repo, "podinfo")
	if err != nil {
		t.Fatalf("versions after ttl: %v", err)
	}
	if hits != 2 {
		t.Fatalf("hits = %d, want a refetch once the ttl passed", hits)
	}
}

func TestVersionsReportsAFetchFailure(t *testing.T) {
	cache, _ := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	})

	_, err := cache.Versions(context.Background(), Repo{URL: "https://example.com"}, "podinfo")

	if err == nil {
		t.Fatal("expected the failed index fetch to surface")
	}
}

func TestVersionsForAChartTheRepoDoesNotCarry(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))

	got, err := cache.Versions(context.Background(), Repo{URL: ts.URL}, "absent")
	if err != nil {
		t.Fatalf("versions: %v", err)
	}
	if len(got) != 0 {
		t.Fatalf("versions = %v, want none for a chart the index does not list", got)
	}
}

func TestLatestReadsTheFirstCachedVersion(t *testing.T) {
	hits := 0
	cache, ts := cacheFor(t, indexHandler(&hits))
	repo := Repo{URL: ts.URL}

	cache.Warm(repo, "podinfo")
	cache.Wait()

	if cache.Latest(repo, "podinfo") != "6.15.1" {
		t.Fatalf("latest = %q, want the newest of the sorted list", cache.Latest(repo, "podinfo"))
	}
	if cache.Latest(repo, "absent") != "" {
		t.Fatalf("latest = %q, want nothing for an unknown chart", cache.Latest(repo, "absent"))
	}
}

func TestCheckFetchable(t *testing.T) {
	cases := []struct {
		raw string
		ok  bool
	}{
		{"https://charts.example.com", true},
		{"http://127.0.0.1:8879", true},
		{"oci://registry.example.com/team", true},
		{"ftp://charts.example.com", false},
		{"https://", false},
		{"://bad", false},
	}
	for _, tc := range cases {
		err := CheckFetchable(tc.raw)
		if tc.ok && err != nil {
			t.Fatalf("CheckFetchable(%q) = %v, want it allowed", tc.raw, err)
		}
		if !tc.ok && err == nil {
			t.Fatalf("CheckFetchable(%q) allowed, want it refused", tc.raw)
		}
	}
}

func TestValidVersion(t *testing.T) {
	cases := []struct {
		version string
		ok      bool
	}{
		{"6.15.1", true},
		{"v1.2.3", true},
		{"1.3.0_20260101", true},
		{"not-semver", false},
		{"--repo", false},
		{"", false},
	}
	for _, tc := range cases {
		if ValidVersion(tc.version) != tc.ok {
			t.Fatalf("ValidVersion(%q) = %v, want %v", tc.version, !tc.ok, tc.ok)
		}
	}
}

func TestNewerComparesOCIBuildMetadataTags(t *testing.T) {
	if !Newer("1.9.0_20260101", "1.10.0_20260102") {
		t.Fatal("an outdated OCI release was reported as up to date; maxVersion hands back the raw underscore tag")
	}
}

func TestNewerRefusesAnUpToDateOCITag(t *testing.T) {
	if Newer("1.10.0_20260102", "1.10.0_20260102") {
		t.Fatal("a release on the newest tag was reported as outdated")
	}
}

func TestNewerStillRefusesGarbage(t *testing.T) {
	if Newer("1.0.0_build_2", "2.0.0") {
		t.Fatal("a tag that is not a version was compared anyway")
	}
}

func TestSameRegistryAcceptsWhatBelongsToTheSameSite(t *testing.T) {
	cases := []struct {
		name     string
		realm    string
		registry string
		want     bool
	}{
		{name: "the very same authority", realm: "ghcr.io", registry: "ghcr.io", want: true},
		{name: "the same host on another port", realm: "ghcr.io:443", registry: "ghcr.io", want: true},
		{name: "a sibling under one domain", realm: "auth.ghcr.io", registry: "registry.ghcr.io", want: true},
		{name: "different sites under a two label public suffix", realm: "auth.attacker.co.uk", registry: "registry.example.co.uk", want: false},
		{name: "a different site", realm: "auth.example.com", registry: "ghcr.io", want: false},
		{name: "an ip against a name", realm: "127.0.0.1", registry: "ghcr.io", want: false},
		{name: "the same ip twice", realm: "127.0.0.1:5000", registry: "127.0.0.1", want: true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := sameRegistry(tc.realm, tc.registry); got != tc.want {
				t.Fatalf("sameRegistry(%q, %q) = %v, want %v", tc.realm, tc.registry, got, tc.want)
			}
		})
	}
}

func TestHostOnlyDropsThePortWhenThereIsOne(t *testing.T) {
	if got := hostOnly("ghcr.io:443"); got != "ghcr.io" {
		t.Fatalf("host = %q, want ghcr.io", got)
	}
	if got := hostOnly("ghcr.io"); got != "ghcr.io" {
		t.Fatalf("host = %q, want the authority unchanged", got)
	}
}

func TestParentDomainUsesTheRegistrableDomain(t *testing.T) {
	if got := parentDomain("auth.docker.example.com"); got != "example.com" {
		t.Fatalf("parent = %q, want example.com", got)
	}
	if got := parentDomain("auth.docker.example.co.uk"); got != "example.co.uk" {
		t.Fatalf("parent = %q, want example.co.uk", got)
	}
	if got := parentDomain("localhost"); got != "localhost" {
		t.Fatalf("parent = %q, want the single label unchanged", got)
	}
}

func TestTokenEndpointRefusesAChallengeItCannotUse(t *testing.T) {
	registry, err := url.Parse("https://ghcr.io")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	cases := []struct {
		name   string
		params map[string]string
		want   string
	}{
		{name: "no realm at all", params: map[string]string{}, want: "no bearer realm"},
		{name: "a realm that is not a url", params: map[string]string{"realm": "://"}, want: "bearer realm"},
		{name: "a realm that is not http", params: map[string]string{"realm": "ftp://ghcr.io/token"}, want: "not an http url"},
		{
			name:   "a realm on another site",
			params: map[string]string{"realm": "https://auth.elsewhere.com/token"},
			want:   "does not belong to ghcr.io",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := tokenEndpoint(tc.params, registry)

			if err == nil {
				t.Fatal("tokenEndpoint returned nil error")
			}
			if !strings.Contains(err.Error(), tc.want) {
				t.Fatalf("error = %q, want it to mention %q", err.Error(), tc.want)
			}
		})
	}
}

func TestCheckHostRefusesAnEmptyHost(t *testing.T) {
	err := checkHost("")

	if err == nil {
		t.Fatal("checkHost returned nil error for an empty host")
	}
	if err.Error() != "repository url has no host" {
		t.Fatalf("error = %q", err.Error())
	}
}

func TestSearchListsWhatTheIndexOffers(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`entries:
  podinfo:
    - version: 6.10.0
      description: a tiny greeter
    - version: 6.9.0
      description: an older greeter
  redis:
    - version: 20.0.1
      description: an in-memory store
`))
	})

	found, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "pod", 10)
	if err != nil {
		t.Fatalf("search: %v", err)
	}

	if len(found) != 1 {
		t.Fatalf("found = %+v, want only podinfo", found)
	}
	if found[0].Version != "6.10.0" {
		t.Fatalf("version = %q, want the newest", found[0].Version)
	}
	if found[0].Description != "a tiny greeter" {
		t.Fatalf("description = %q, want the newest version's", found[0].Description)
	}
}

func TestSearchMatchesTheDescriptionToo(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`entries:
  redis:
    - version: 20.0.1
      description: an in-memory store
`))
	})

	found, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "in-memory", 10)
	if err != nil {
		t.Fatalf("search: %v", err)
	}

	if len(found) != 1 || found[0].Name != "redis" {
		t.Fatalf("found = %+v", found)
	}
}

func TestSearchPutsNameMatchesBeforeDescriptionMatches(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`entries:
  cache-proxy:
    - version: 1.0.0
      description: a proxy
  redis:
    - version: 20.0.1
      description: a cache
`))
	})

	found, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "cache", 10)
	if err != nil {
		t.Fatalf("search: %v", err)
	}

	if len(found) != 2 {
		t.Fatalf("found = %+v", found)
	}
	if found[0].Name != "cache-proxy" {
		t.Fatalf("first = %q, want the name match first", found[0].Name)
	}
}

func TestSearchKeepsAtMostTheLimit(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`entries:
  chart-a:
    - version: 1.0.0
  chart-b:
    - version: 1.0.0
  chart-c:
    - version: 1.0.0
`))
	})

	found, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "chart", 2)
	if err != nil {
		t.Fatalf("search: %v", err)
	}

	if len(found) != 2 {
		t.Fatalf("found = %d, want the limit", len(found))
	}
}

func TestAnIndexLargerThanItsLimitIsNotAcceptedPartially(t *testing.T) {
	body := "entries:\n  web:\n    - version: 1.0.0\n"

	_, err := parseBoundedIndex(strings.NewReader(body+"extra"), len(body))

	if err == nil || !strings.Contains(err.Error(), "larger than") {
		t.Fatalf("err = %v", err)
	}
}

func TestJSONLargerThanItsLimitIsNotAcceptedPartially(t *testing.T) {
	body := `{"tags":["1.0.0"]}`
	var doc struct {
		Tags []string `json:"tags"`
	}

	err := decodeBoundedJSON(strings.NewReader(body+"extra"), len(body), &doc)

	if err == nil || !strings.Contains(err.Error(), "larger than") {
		t.Fatalf("err = %v", err)
	}
}

func TestRepositoryResponseLimitAcceptsTheBoundaryAndRejectsOneMoreByte(t *testing.T) {
	if _, err := readBounded(strings.NewReader(strings.Repeat("x", maxBodyBytes)), maxBodyBytes); err != nil {
		t.Fatalf("boundary response: %v", err)
	}
	_, err := readBounded(strings.NewReader(strings.Repeat("x", maxBodyBytes+1)), maxBodyBytes)
	if err == nil || err.Error() != "response body is larger than 8388608 bytes" {
		t.Fatalf("error = %v", err)
	}
}

func TestAnIndexReadFailureIsNotAcceptedPartially(t *testing.T) {
	_, err := parseBoundedIndex(&interruptedReader{}, maxBodyBytes)

	if err == nil || !strings.Contains(err.Error(), "repository response interrupted") {
		t.Fatalf("error = %v, want the interrupted response reported", err)
	}
}

func TestAnEmptySearchListsEverything(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`entries:
  chart-a:
    - version: 1.0.0
  chart-b:
    - version: 1.0.0
`))
	})

	found, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "", 0)
	if err != nil {
		t.Fatalf("search: %v", err)
	}

	if len(found) != 2 {
		t.Fatalf("found = %+v, want both", found)
	}
}

func TestSearchRefusesAnOCIRegistry(t *testing.T) {
	cache := New(context.Background(), http.DefaultClient, DefaultTTL)

	_, err := cache.Search(context.Background(), Repo{URL: "oci://registry.example.com/charts", OCI: true}, "podinfo", 10)

	if err == nil {
		t.Fatal("an oci registry was listed")
	}
	if !strings.Contains(err.Error(), "name the chart instead") {
		t.Fatalf("error = %v", err)
	}
}

func TestSearchReportsAnIndexItCannotRead(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	})

	_, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "podinfo", 10)

	if err == nil {
		t.Fatal("a missing index reported success")
	}
}

func TestSearchServesTheSecondAskFromTheCache(t *testing.T) {
	asks := 0
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		asks++
		_, _ = w.Write([]byte("entries:\n  podinfo:\n    - version: 6.10.0\n"))
	})

	_, first := cache.Search(context.Background(), Repo{URL: ts.URL}, "podinfo", 10)
	_, second := cache.Search(context.Background(), Repo{URL: ts.URL}, "pod", 10)

	if first != nil || second != nil {
		t.Fatalf("search: %v %v", first, second)
	}
	if asks != 1 {
		t.Fatalf("index fetched %d times, want once", asks)
	}
}

func TestSearchMatchesTheMiddleOfAName(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`entries:
  podinfo:
    - version: 6.14.1
      description: a tiny greeter
  kube-prometheus-stack:
    - version: 88.3.0
      description: a bundle
`))
	})

	found, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "prometheus", 10)
	if err != nil {
		t.Fatalf("search: %v", err)
	}

	if len(found) != 1 || found[0].Name != "kube-prometheus-stack" {
		t.Fatalf("found = %+v, want the chart whose name carries the word", found)
	}
}

func TestAChartWithNoUsableVersionIsLeftOutOfTheCatalogue(t *testing.T) {
	cache, ts := cacheFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`entries:
  podinfo:
    - version: 6.14.1
  broken:
    - version: not-a-version
  prereleased:
    - version: 1.0.0-rc1
`))
	})

	found, err := cache.Search(context.Background(), Repo{URL: ts.URL}, "", 10)
	if err != nil {
		t.Fatalf("search: %v", err)
	}

	if len(found) != 1 || found[0].Name != "podinfo" {
		t.Fatalf("found = %+v, want only the chart with a version helm could install", found)
	}
}