quorate/services/project-delivery/controller/forgejo_test.go

255 lines
9.5 KiB
Go

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:[email protected]/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))
}