206 lines
7.6 KiB
Go
206 lines
7.6 KiB
Go
package gitea
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"go.maximumdirect.net/releasekit/internal/forge"
|
|
)
|
|
|
|
func TestBackendOpenAssetBoundsRedirects(t *testing.T) {
|
|
for _, hops := range []int{0, 10, 11} {
|
|
t.Run(strconv.Itoa(hops), func(t *testing.T) {
|
|
var requests atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests.Add(1)
|
|
if got := r.Header.Get("Authorization"); got != "token fixture-secret-token" {
|
|
t.Errorf("authorization = %q", got)
|
|
}
|
|
step, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/"))
|
|
if err != nil {
|
|
t.Errorf("invalid path %q", r.URL.Path)
|
|
return
|
|
}
|
|
if step < hops {
|
|
http.Redirect(w, r, fmt.Sprintf("/%d", step+1), http.StatusFound)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, "asset")
|
|
}))
|
|
defer server.Close()
|
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
|
stream, err := backend.OpenAsset(t.Context(), assetHandle{downloadURL: "/0"})
|
|
if hops <= 10 {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, readErr := io.ReadAll(stream)
|
|
closeErr := stream.Close()
|
|
if readErr != nil || closeErr != nil || string(body) != "asset" || requests.Load() != int32(hops+1) {
|
|
t.Fatalf("body = %q, read = %v, close = %v, requests = %d", body, readErr, closeErr, requests.Load())
|
|
}
|
|
return
|
|
}
|
|
var failure *forge.Failure
|
|
if !errors.As(err, &failure) || failure.Kind != forge.FailureUnverifiable || !strings.Contains(err.Error(), "redirect limit") || requests.Load() != 11 || stream != nil {
|
|
t.Fatalf("stream = %v, error = %v, requests = %d", stream, err, requests.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBackendOpenAssetRedirectReturnsToOrigin(t *testing.T) {
|
|
var origin *httptest.Server
|
|
external := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("Authorization"); got != "" {
|
|
t.Errorf("cross-origin authorization = %q", got)
|
|
}
|
|
http.Redirect(w, r, origin.URL+"/final", http.StatusFound)
|
|
}))
|
|
defer external.Close()
|
|
origin = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("Authorization"); got != "token fixture-secret-token" {
|
|
t.Errorf("origin authorization = %q", got)
|
|
}
|
|
if r.URL.Path == "/start" {
|
|
http.Redirect(w, r, external.URL+"/away", http.StatusFound)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, "asset")
|
|
}))
|
|
defer origin.Close()
|
|
stream, err := NewBackend(testDestination(origin.URL), origin.Client()).OpenAsset(t.Context(), assetHandle{downloadURL: "/start"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stream.Close()
|
|
if body, err := io.ReadAll(stream); err != nil || string(body) != "asset" {
|
|
t.Fatalf("body = %q, error = %v", body, err)
|
|
}
|
|
}
|
|
|
|
func TestBackendOpenAssetRejectsRedirectCycle(t *testing.T) {
|
|
var requests atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests.Add(1)
|
|
http.Redirect(w, r, "/loop#different-fragment", http.StatusFound)
|
|
}))
|
|
defer server.Close()
|
|
stream, err := NewBackend(testDestination(server.URL), server.Client()).OpenAsset(t.Context(), assetHandle{downloadURL: "/loop"})
|
|
var failure *forge.Failure
|
|
if !errors.As(err, &failure) || failure.Kind != forge.FailureUnverifiable || !strings.Contains(err.Error(), "loop") || requests.Load() != 1 || stream != nil {
|
|
t.Fatalf("stream = %v, error = %v, requests = %d", stream, err, requests.Load())
|
|
}
|
|
}
|
|
|
|
func TestBackendListAssetsRejectsMalformedAttachmentList(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet || r.URL.Path != "/api/v1/repos/owner/project/releases/10/assets" {
|
|
t.Errorf("request = %s %s", r.Method, r.URL.Path)
|
|
}
|
|
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
|
|
t.Errorf("authorization = %q, want %q", got, want)
|
|
}
|
|
_, _ = io.WriteString(w, "null")
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := NewBackend(testDestination(server.URL), server.Client()).ListAssets(context.Background(), releaseHandle{id: 10})
|
|
var failure *forge.Failure
|
|
if !errors.As(err, &failure) || failure.Kind != forge.FailureMalformed {
|
|
t.Fatalf("ListAssets() error = %v, want malformed attachment response", err)
|
|
}
|
|
}
|
|
|
|
func TestBackendOpenAssetFollowsSameOriginRedirectWithAuthorization(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 at %s = %q, want %q", r.URL.Path, got, want)
|
|
}
|
|
switch r.URL.Path {
|
|
case "/files/redirect":
|
|
http.Redirect(w, r, "/files/content", http.StatusFound)
|
|
case "/files/content":
|
|
_, _ = io.WriteString(w, "streamed asset")
|
|
default:
|
|
t.Errorf("unexpected path %q", r.URL.Path)
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
|
stream, err := backend.OpenAsset(context.Background(), assetHandle{downloadURL: "/files/redirect"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, readErr := io.ReadAll(stream)
|
|
closeErr := stream.Close()
|
|
if readErr != nil || closeErr != nil || string(content) != "streamed asset" {
|
|
t.Fatalf("OpenAsset() content = %q, read error = %v, close error = %v", content, readErr, closeErr)
|
|
}
|
|
}
|
|
|
|
func TestBackendOpenAssetDoesNotForwardAuthorizationAcrossOrigin(t *testing.T) {
|
|
for _, mode := range []string{"direct", "redirect"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
var externalRequests atomic.Int32
|
|
external := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
externalRequests.Add(1)
|
|
if got := r.Header.Get("Authorization"); got != "" {
|
|
t.Errorf("external authorization = %q, want empty", got)
|
|
}
|
|
_, _ = io.WriteString(w, "external asset")
|
|
}))
|
|
defer external.Close()
|
|
|
|
origin := 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("origin authorization = %q, want %q", got, want)
|
|
}
|
|
http.Redirect(w, r, external.URL+"/content", http.StatusFound)
|
|
}))
|
|
defer origin.Close()
|
|
|
|
downloadURL := external.URL + "/content"
|
|
if mode == "redirect" {
|
|
downloadURL = "/redirect"
|
|
}
|
|
backend := NewBackend(testDestination(origin.URL), origin.Client())
|
|
stream, err := backend.OpenAsset(context.Background(), assetHandle{downloadURL: downloadURL})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, readErr := io.ReadAll(stream)
|
|
closeErr := stream.Close()
|
|
if readErr != nil || closeErr != nil || string(content) != "external asset" || externalRequests.Load() != 1 {
|
|
t.Fatalf("OpenAsset() content = %q, read error = %v, close error = %v, external requests = %d", content, readErr, closeErr, externalRequests.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBackendListAssetsDecodesAttachmentMetadata(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_ = json.NewEncoder(w).Encode([]assetResponse{{ID: 4, Name: "app.bin", Size: 4, BrowserDownloadURL: "/files/app"}})
|
|
}))
|
|
defer server.Close()
|
|
|
|
assets, err := NewBackend(testDestination(server.URL), server.Client()).ListAssets(context.Background(), releaseHandle{id: 10})
|
|
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)
|
|
}
|
|
}
|