package main import ( "bytes" "context" "encoding/base64" "fmt" "io" "net/http" "strconv" "strings" "testing" ) type roundTripFunc func(*http.Request) (*http.Response, error) func (roundTrip roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return roundTrip(request) } func TestParseAdvertisement(t *testing.T) { revision := "0123456789abcdef0123456789abcdef01234567" tagObject := "89abcdef0123456789abcdef0123456789abcdef" content := append(packet("# service=git-upload-pack\n"), []byte("0000")...) content = append(content, packet(revision+" refs/heads/main\x00multi_ack\n")...) content = append(content, packet(tagObject+" refs/tags/v1\n")...) content = append(content, packet(revision+" refs/tags/v1^{}\n")...) content = append(content, []byte("0000")...) refs, err := parseAdvertisement(bytes.NewReader(content)) if err != nil { t.Fatal(err) } if len(refs) != 2 || refs["refs/heads/main"] != revision || refs["refs/tags/v1"] != revision { t.Fatalf("refs = %#v", refs) } } func TestParseAdvertisementKeepsLightweightTag(t *testing.T) { revision := "0123456789abcdef0123456789abcdef01234567" content := append(packet(revision+" refs/tags/v1\x00multi_ack\n"), []byte("0000")...) refs, err := parseAdvertisement(bytes.NewReader(content)) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs["refs/tags/v1"] != revision { t.Fatalf("refs = %#v", refs) } } func TestRepositoryPath(t *testing.T) { path, err := repositoryPath("https://git.quorate.dev/quorate/nuthatch.git") if err != nil { t.Fatal(err) } if path != "quorate/nuthatch.git" { t.Fatalf("path = %q", path) } } func TestForgejoClientAuthenticatesRefAdvertisement(t *testing.T) { revision := "0123456789abcdef0123456789abcdef01234567" transport := roundTripFunc(func(request *http.Request) (*http.Response, error) { if request.URL.RequestURI() != "/quorate/nuthatch.git/info/refs?service=git-upload-pack" { t.Fatalf("request URI = %q", request.URL.RequestURI()) } expected := "Basic " + base64.StdEncoding.EncodeToString([]byte("oauth2:test-token")) if request.Header.Get("Authorization") != expected { t.Fatal("missing source authorization") } content := append(packet(revision+" refs/heads/main\x00multi_ack\n"), []byte("0000")...) return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": {"application/x-git-upload-pack-advertisement"}}, Body: io.NopCloser(strings.NewReader(string(content))), }, nil }) client, err := newForgejoClient("https://forgejo.test", &http.Client{Transport: transport}, "test-token") if err != nil { t.Fatal(err) } refs, err := client.Refs(context.Background(), "https://git.quorate.dev/quorate/nuthatch") if err != nil { t.Fatal(err) } if refs["refs/heads/main"] != revision { t.Fatalf("refs = %#v", refs) } } func TestForgejoClientIdentifiesSelectedPathTree(t *testing.T) { first := "0123456789abcdef0123456789abcdef01234567" second := "89abcdef0123456789abcdef0123456789abcdef" packages := "1111111111111111111111111111111111111111" runner := "2222222222222222222222222222222222222222" transport := roundTripFunc(func(request *http.Request) (*http.Response, error) { tree := request.URL.Path[strings.LastIndex(request.URL.Path, "/")+1:] var body string switch tree { case first: body = fmt.Sprintf(`{"tree":[{"path":"docs","mode":"040000","type":"tree","sha":"%s"},{"path":"packages","mode":"040000","type":"tree","sha":"%s"}],"page":1,"total_count":2}`, second, packages) case second: body = fmt.Sprintf(`{"tree":[{"path":"packages","mode":"040000","type":"tree","sha":"%s"}],"page":1,"total_count":1}`, packages) case packages: body = fmt.Sprintf(`{"tree":[{"path":"fsharp-runner","mode":"040000","type":"tree","sha":"%s"}],"page":1,"total_count":1}`, runner) default: t.Fatalf("unexpected tree %q", tree) } return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body))}, nil }) client, err := newForgejoClient("https://forgejo.test", &http.Client{Transport: transport}, "") if err != nil { t.Fatal(err) } firstIdentity, err := client.PathIdentity(context.Background(), "https://git.quorate.dev/ms2k/quorate", first, []string{"packages/fsharp-runner"}) if err != nil { t.Fatal(err) } secondIdentity, err := client.PathIdentity(context.Background(), "https://git.quorate.dev/ms2k/quorate", second, []string{"packages/fsharp-runner"}) if err != nil { t.Fatal(err) } if firstIdentity != secondIdentity { t.Fatalf("identities differ: %q != %q", firstIdentity, secondIdentity) } } func TestForgejoClientReadsAllTreePages(t *testing.T) { revision := "0123456789abcdef0123456789abcdef01234567" target := "89abcdef0123456789abcdef0123456789abcdef" requests := 0 transport := roundTripFunc(func(request *http.Request) (*http.Response, error) { requests++ page, _ := strconv.Atoi(request.URL.Query().Get("page")) entries := make([]string, 0, 100) if page == 1 { for index := range 100 { entries = append(entries, fmt.Sprintf(`{"path":"entry-%03d","mode":"100644","type":"blob","sha":"%s"}`, index, target)) } } else if page == 2 { entries = append(entries, fmt.Sprintf(`{"path":"selected","mode":"100755","type":"blob","sha":"%s"}`, target)) } body := fmt.Sprintf(`{"tree":[%s],"page":%d,"total_count":101}`, strings.Join(entries, ","), page) return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body))}, nil }) client, err := newForgejoClient("https://forgejo.test", &http.Client{Transport: transport}, "") if err != nil { t.Fatal(err) } if _, err := client.PathIdentity(context.Background(), "https://git.quorate.dev/ms2k/quorate", revision, []string{"selected"}); err != nil { t.Fatal(err) } if requests != 2 { t.Fatalf("tree requests = %d", requests) } } func TestForgejoClientIncludesMissingPathsInIdentity(t *testing.T) { revision := "0123456789abcdef0123456789abcdef01234567" transport := roundTripFunc(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"tree":[],"page":1,"total_count":0}`))}, nil }) client, err := newForgejoClient("https://forgejo.test", &http.Client{Transport: transport}, "") if err != nil { t.Fatal(err) } first, err := client.PathIdentity(context.Background(), "https://git.quorate.dev/ms2k/quorate", revision, []string{"missing-a"}) if err != nil { t.Fatal(err) } second, err := client.PathIdentity(context.Background(), "https://git.quorate.dev/ms2k/quorate", revision, []string{"missing-b"}) if err != nil { t.Fatal(err) } if first == second { t.Fatal("different missing path scopes received the same identity") } } func TestForgejoRouterKeepsCredentialsWithinAuthority(t *testing.T) { revision := "0123456789abcdef0123456789abcdef01234567" publicRequests := 0 privateRequests := 0 client := func(expectedToken string, requests *int) *http.Client { return &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { (*requests)++ expected := "Basic " + base64.StdEncoding.EncodeToString([]byte("oauth2:"+expectedToken)) if request.Header.Get("Authorization") != expected { t.Fatalf("authorization = %q", request.Header.Get("Authorization")) } content := append(packet(revision+" refs/heads/main\x00multi_ack\n"), []byte("0000")...) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(content)), }, nil })} } public, err := newForgejoClient("http://public.forgejo.test:3000", client("public-token", &publicRequests), "public-token") if err != nil { t.Fatal(err) } private, err := newForgejoClient("http://private.forgejo.test:3000", client("private-token", &privateRequests), "private-token") if err != nil { t.Fatal(err) } router, err := newForgejoRouter( forgejoRoute{authority: "git.quorate.dev", source: public}, forgejoRoute{authority: "forge.quorate.internal", source: private}, ) if err != nil { t.Fatal(err) } if _, err := router.Refs(context.Background(), "https://git.quorate.dev/ms2k/project"); err != nil { t.Fatal(err) } if publicRequests != 1 || privateRequests != 0 { t.Fatalf("requests public=%d private=%d", publicRequests, privateRequests) } if _, err := router.Refs(context.Background(), "https://forge.quorate.internal/quorate/cluster"); err != nil { t.Fatal(err) } if publicRequests != 1 || privateRequests != 1 { t.Fatalf("requests public=%d private=%d", publicRequests, privateRequests) } if _, err := router.Refs(context.Background(), "https://unknown.example/owner/repository"); err == nil { t.Fatal("unknown authority received a Forgejo route") } if publicRequests != 1 || privateRequests != 1 { t.Fatalf("unknown authority sent a request: public=%d private=%d", publicRequests, privateRequests) } } func TestForgejoRouterRejectsDuplicateAuthority(t *testing.T) { client, err := newForgejoClient("http://forgejo.test:3000", http.DefaultClient, "") if err != nil { t.Fatal(err) } if _, err := newForgejoRouter( forgejoRoute{authority: "git.quorate.dev", source: client}, forgejoRoute{authority: "GIT.QUORATE.DEV", source: client}, ); err == nil { t.Fatal("duplicate repository authority was accepted") } } func TestRepositoryAuthorityRejectsCredentialBearingURL(t *testing.T) { if _, err := repositoryAuthority("https://oauth2:token@git.quorate.dev/quorate/nuthatch"); err == nil { t.Fatal("credential-bearing repository URL was accepted") } } func packet(value string) []byte { return []byte(fmt.Sprintf("%04x%s", len(value)+4, value)) }