package fetch import ( "context" "errors" "net/http" "net/http/httptest" "path/filepath" "testing" "github.com/grmrgecko/repo-sync/internal/testrepos" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestParseMirrorlist verifies URL extraction, comment skipping, and // deduplication. func TestParseMirrorlist(t *testing.T) { body := "# comment\n\nhttp://mirror1.example.com/repo/\nhttps://mirror2.example.com/repo\nhttp://mirror1.example.com/repo\n/bare/path\nnot a url\n" bases := ParseMirrorlist([]byte(body)) want := []string{"http://mirror1.example.com/repo", "https://mirror2.example.com/repo"} if len(bases) != len(want) { t.Fatalf("got %v, want %v", bases, want) } for i := range want { if bases[i] != want[i] { t.Errorf("bases[%d] = %q, want %q", i, bases[i], want[i]) } } } // TestSourceFailover verifies a dead first mirror fails over to a working // one and that missing files surface the not-found sentinel. func TestSourceFailover(t *testing.T) { www := t.TempDir() testrepos.BuildRPMRepo(t, filepath.Join(www, "el9")) srv := testrepos.ServeDir(t, www) src := NewSource([]string{"http://127.0.0.1:1/el9", srv.URL + "/el9"}) resp, err := src.Get(context.Background(), "repodata/repomd.xml", GetOptions{}) if err != nil { t.Fatal("failover did not reach the working mirror:", err) } resp.Body.Close() if src.active != 1 { t.Errorf("active mirror = %d, want 1", src.active) } if _, err := src.Get(context.Background(), "no/such/file", GetOptions{}); !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound, got %v", err) } } // TestSourceForbidden verifies HTTP 403 remains distinct from a missing file // so only callers fetching optional object-store keys can ignore it. func TestSourceForbidden(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) })) t.Cleanup(srv.Close) _, err := NewSource([]string{srv.URL}).Get(context.Background(), "private", GetOptions{}) require.Error(t, err) assert.ErrorIs(t, err, ErrForbidden) assert.NotErrorIs(t, err, ErrNotFound) } // TestParseMetalink verifies base URL extraction from metalink documents. func TestParseMetalink(t *testing.T) { body := `` + `` + `https://mirror1.example.com/fedora/repodata/repomd.xml` + `rsync://mirror2.example.com/fedora/repodata/repomd.xml` + `http://mirror3.example.com/fedora/repodata/repomd.xml` + `https://mirror1.example.com/fedora/repodata/repomd.xml` + `` bases := ParseMetalink([]byte(body)) want := []string{"https://mirror1.example.com/fedora", "http://mirror3.example.com/fedora"} if len(bases) != len(want) { t.Fatalf("got %v, want %v", bases, want) } for i := range want { if bases[i] != want[i] { t.Errorf("bases[%d] = %q, want %q", i, bases[i], want[i]) } } if ParseMetalink([]byte("not xml at all")) != nil { t.Error("non-XML input produced bases") } } // TestSourceNoBases verifies a Source with no mirrors reports an error and // no response. func TestSourceNoBases(t *testing.T) { src := NewSource(nil) resp, err := src.Get(context.Background(), "anything", GetOptions{}) if err == nil { t.Fatal("expected an error for a source without bases") } if resp != nil { t.Error("response must be nil on error") } } // TestSourceRebase verifies suffix trimming for suite-relative sources. func TestSourceRebase(t *testing.T) { src := NewSource([]string{"http://example.com/debian/dists/test", "http://other.example.com/mirror"}) src.Rebase("dists/test") if src.bases[0] != "http://example.com/debian" { t.Errorf("bases[0] = %q", src.bases[0]) } if src.bases[1] != "http://other.example.com/mirror" { t.Errorf("bases[1] changed unexpectedly: %q", src.bases[1]) } }