Files
releasekit/internal/gitea/backend_test.go

248 lines
9.5 KiB
Go

package gitea
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"go.maximumdirect.net/releasekit/internal/forge"
)
func TestBackendTagCommitUsesEscapedFullTagAndAuthentication(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.RequestURI, "/api/v1/repos/owner/project/tags/v1.2.3%2Frc.1"; got != want {
t.Errorf("request URI = %q, want %q", got, want)
}
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
t.Errorf("authorization = %q, want %q", got, want)
}
_ = json.NewEncoder(w).Encode(map[string]any{"commit": map[string]string{"sha": "abcdef"}})
}))
defer server.Close()
backend := NewBackend(testDestination(server.URL), server.Client())
commit, err := backend.TagCommit(context.Background(), "v1.2.3/rc.1")
if err != nil || commit != "abcdef" {
t.Fatalf("TagCommit() = %q, %v", commit, err)
}
}
func TestBackendFindReleaseReturnsNeutralNotFound(t *testing.T) {
server := httptest.NewServer(http.NotFoundHandler())
defer server.Close()
backend := NewBackend(testDestination(server.URL), server.Client())
_, err := backend.FindRelease(context.Background(), "v1.2.3")
var failure *forge.Failure
if !errors.As(err, &failure) || failure.Kind != forge.FailureNotFound {
t.Fatalf("FindRelease() error = %v, want neutral not-found failure", err)
}
var cause *Failure
if !errors.As(err, &cause) || cause.StatusCode != http.StatusNotFound {
t.Fatalf("FindRelease() error cause = %v, want Gitea 404 diagnostic", err)
}
}
func TestBackendFindReleaseDoesNotRequireWriteResponseFields(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(releaseResponse{TagName: "v1.2.3", Name: "Release"})
}))
defer server.Close()
backend := NewBackend(testDestination(server.URL), server.Client())
release, err := backend.FindRelease(context.Background(), "v1.2.3")
if err != nil {
t.Fatal(err)
}
if release.Tag != "v1.2.3" || release.Title != "Release" || release.Handle == nil {
t.Fatalf("FindRelease() = %#v", release)
}
}
func TestBackendListsMetadataAndOpensCallerOwnedAssetStream(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
t.Errorf("authorization = %q, want %q", got, want)
}
switch r.URL.Path {
case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
_ = json.NewEncoder(w).Encode(releaseResponse{ID: 12, TagName: "v1.2.3"})
case "/api/v1/repos/owner/project/releases/12/assets":
_ = json.NewEncoder(w).Encode([]assetResponse{{ID: 4, Name: "app.bin", Size: 4, BrowserDownloadURL: "/files/app"}})
case "/files/app":
_, _ = io.WriteString(w, "data")
default:
t.Errorf("unexpected path %q", r.URL.Path)
http.NotFound(w, r)
}
}))
defer server.Close()
backend := NewBackend(testDestination(server.URL), server.Client())
release, err := backend.FindRelease(context.Background(), "v1.2.3")
if err != nil {
t.Fatal(err)
}
assets, err := backend.ListAssets(context.Background(), release.Handle)
if err != nil {
t.Fatal(err)
}
if len(assets) != 1 || assets[0].Filename != "app.bin" || assets[0].Size != 4 || assets[0].Handle == nil {
t.Fatalf("ListAssets() = %#v", assets)
}
stream, err := backend.OpenAsset(context.Background(), assets[0].Handle)
if err != nil {
t.Fatal(err)
}
content, readErr := io.ReadAll(stream)
closeErr := stream.Close()
if readErr != nil || closeErr != nil || string(content) != "data" {
t.Fatalf("OpenAsset() content = %q, read error = %v, close error = %v", content, readErr, closeErr)
}
}
func TestBackendRejectsAssetHandleFromAnotherBackendType(t *testing.T) {
backend := NewBackend(testDestination("http://example.test"), nil)
_, err := backend.OpenAsset(context.Background(), strings.NewReader("asset"))
var failure *forge.Failure
if !errors.As(err, &failure) || failure.Kind != forge.FailureInvalid {
t.Fatalf("OpenAsset() error = %v, want invalid handle", err)
}
}
func TestBackendPreflightUsesAuthenticatedRepositoryReadsAndIgnoresCodePushPermission(t *testing.T) {
var requests []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("method = %q, want GET", r.Method)
}
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
t.Errorf("authorization = %q, want %q", got, want)
}
requests = append(requests, r.URL.Path)
writeRepository := func() {
_ = json.NewEncoder(w).Encode(map[string]any{
"id": 31, "name": "project", "full_name": "owner/project",
"owner": map[string]string{"login": "owner"},
"permissions": map[string]bool{"admin": false, "push": false, "pull": true},
})
}
switch r.URL.Path {
case "/api/v1/repos/owner/project", "/api/v1/repositories/31":
writeRepository()
default:
t.Errorf("unexpected path %q", r.URL.Path)
http.NotFound(w, r)
}
}))
defer server.Close()
if err := NewBackend(testDestination(server.URL), server.Client()).Preflight(context.Background()); err != nil {
t.Fatal(err)
}
if len(requests) != 2 || requests[0] != "/api/v1/repos/owner/project" || requests[1] != "/api/v1/repositories/31" {
t.Fatalf("preflight requests = %v, want both repository reads", requests)
}
}
func TestBackendPreflightRequiresCredentialWithoutMakingRequests(t *testing.T) {
var requests int
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { requests++ }))
defer server.Close()
destination := testDestination(server.URL)
destination.Token = " \t"
err := NewBackend(destination, server.Client()).Preflight(context.Background())
var failure *forge.PreflightFailure
if !errors.As(err, &failure) || failure.Kind != forge.PreflightMissingCredential {
t.Fatalf("Preflight() error = %v, want missing credential", err)
}
if requests != 0 {
t.Fatalf("made %d requests without a credential", requests)
}
}
func TestBackendPreflightDoesNotAcceptPublicLookupWithoutAuthenticatedAccess(t *testing.T) {
var requests []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests = append(requests, r.URL.Path)
if r.URL.Path == "/api/v1/repos/owner/project" {
_ = json.NewEncoder(w).Encode(map[string]any{"id": 31, "name": "project", "full_name": "owner/project", "owner": map[string]string{"login": "owner"}})
return
}
http.Error(w, "token rejected", http.StatusUnauthorized)
}))
defer server.Close()
err := NewBackend(testDestination(server.URL), server.Client()).Preflight(context.Background())
var failure *forge.PreflightFailure
if !errors.As(err, &failure) || failure.Kind != forge.PreflightInvalidCredential {
t.Fatalf("Preflight() error = %v, want invalid credential", err)
}
if len(requests) != 2 || requests[1] != "/api/v1/repositories/31" {
t.Fatalf("preflight requests = %v, want authenticated repository read", requests)
}
if strings.Contains(err.Error(), testDestination(server.URL).Token) {
t.Fatalf("preflight error exposed the credential: %v", err)
}
}
func TestBackendPreflightClassifiesDeniedAndHiddenRepositoriesAsInaccessible(t *testing.T) {
for _, status := range []int{http.StatusForbidden, http.StatusNotFound} {
t.Run(http.StatusText(status), func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "repository unavailable", status)
}))
defer server.Close()
err := NewBackend(testDestination(server.URL), server.Client()).Preflight(context.Background())
var failure *forge.PreflightFailure
if !errors.As(err, &failure) || failure.Kind != forge.PreflightInaccessibleDestination {
t.Fatalf("Preflight() error = %v, want inaccessible destination", err)
}
})
}
}
func TestBackendPreflightClassifiesUnverifiableResponsesAsUnavailable(t *testing.T) {
for _, test := range []struct {
name string
respond func(http.ResponseWriter)
}{
{name: "malformed JSON", respond: func(w http.ResponseWriter) { _, _ = io.WriteString(w, "{") }},
{name: "service failure", respond: func(w http.ResponseWriter) { http.Error(w, "temporarily unavailable", http.StatusServiceUnavailable) }},
{name: "wrong repository identity", respond: func(w http.ResponseWriter) {
_ = json.NewEncoder(w).Encode(map[string]any{"id": 31, "name": "other", "full_name": "owner/other", "owner": map[string]string{"login": "owner"}})
}},
} {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { test.respond(w) }))
defer server.Close()
err := NewBackend(testDestination(server.URL), server.Client()).Preflight(context.Background())
var failure *forge.PreflightFailure
if !errors.As(err, &failure) || failure.Kind != forge.PreflightUnavailable {
t.Fatalf("Preflight() error = %v, want unavailable", err)
}
})
}
}
func TestBackendPreflightPreservesCancellation(t *testing.T) {
server := httptest.NewServer(http.NotFoundHandler())
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := NewBackend(testDestination(server.URL), server.Client()).Preflight(ctx)
var failure *forge.PreflightFailure
if !errors.As(err, &failure) || failure.Kind != forge.PreflightUnavailable || !errors.Is(err, context.Canceled) {
t.Fatalf("Preflight() error = %v, want unavailable and context cancellation", err)
}
}