304 lines
11 KiB
Go
304 lines
11 KiB
Go
package gitea
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
const assetListPath = "/api/v1/repos/owner/project/releases/10/assets"
|
|
|
|
func TestBuildManifestSortsNamesAndOmitsEmptySourceOnlyManifest(t *testing.T) {
|
|
manifest, err := BuildManifest([]ExpectedAsset{
|
|
assetFor("zeta.bin", []byte("zeta")),
|
|
assetFor("alpha.bin", []byte("alpha")),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := fmt.Sprintf("%s alpha.bin\n%s zeta.bin\n", digest([]byte("alpha")), digest([]byte("zeta")))
|
|
if string(manifest.Content) != want || manifest.Filename != ManifestFilename || manifest.SHA256 != digest([]byte(want)) || manifest.Size != int64(len(want)) {
|
|
t.Fatalf("manifest = %#v, content %q", manifest, manifest.Content)
|
|
}
|
|
empty, err := BuildManifest(nil)
|
|
if err != nil || empty.Filename != "" || len(empty.Content) != 0 {
|
|
t.Fatalf("empty manifest = %#v, error = %v", empty, err)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsAcceptsMatchingContentAndPreservesUnrelatedFiles(t *testing.T) {
|
|
content := []byte("selected binary")
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
switch r.URL.Path {
|
|
case assetListPath:
|
|
writeAssetList(t, w, []assetResponse{
|
|
{ID: 4, Name: "app-v1.2.3-linux-amd64", Size: int64(len(content)), BrowserDownloadURL: "/files/app"},
|
|
{ID: 8, Name: "notes.txt", Size: 9, BrowserDownloadURL: "/files/notes"},
|
|
})
|
|
case "/files/app":
|
|
_, _ = w.Write(content)
|
|
case "/files/notes":
|
|
_, _ = w.Write([]byte("unrelated"))
|
|
default:
|
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{assetFor("app-v1.2.3-linux-amd64", content)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(inspection.Assets) != 1 || inspection.Assets[0].State != AssetMatching {
|
|
t.Fatalf("inspection = %#v", inspection)
|
|
}
|
|
if len(inspection.Unrelated) != 1 || inspection.Unrelated[0].Filename != "notes.txt" {
|
|
t.Fatalf("unrelated = %#v", inspection.Unrelated)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes(t *testing.T) {
|
|
expected := []ExpectedAsset{assetFor("app.bin", []byte("alpha")), assetFor("other.bin", []byte("beta"))}
|
|
var appDownloads, otherDownloads atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
if r.URL.Path == assetListPath {
|
|
writeAssetList(t, w, []assetResponse{
|
|
{ID: 2, Name: expected[0].Filename, Size: 5, BrowserDownloadURL: "/files/app"},
|
|
{ID: 3, Name: expected[1].Filename, Size: 4, BrowserDownloadURL: "/files/other"},
|
|
})
|
|
return
|
|
}
|
|
if r.URL.Path == "/files/app" {
|
|
appDownloads.Add(1)
|
|
_, _ = w.Write([]byte("bravo"))
|
|
return
|
|
}
|
|
otherDownloads.Add(1)
|
|
_, _ = w.Write([]byte("beta"))
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, expected)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inspection.Assets[0].State != AssetConflicting || inspection.Assets[1].State != AssetMatching || appDownloads.Load() != 1 || otherDownloads.Load() != 1 {
|
|
t.Fatalf("inspection = %#v, downloads = %d and %d", inspection, appDownloads.Load(), otherDownloads.Load())
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsAcceptsFullManifestWhenBinariesAreMissing(t *testing.T) {
|
|
binaries := []ExpectedAsset{assetFor("alpha.bin", []byte("alpha")), assetFor("beta.bin", []byte("beta"))}
|
|
manifest, err := BuildManifest(binaries)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
if r.URL.Path == assetListPath {
|
|
writeAssetList(t, w, []assetResponse{{ID: 3, Name: manifest.Filename, Size: manifest.Size, BrowserDownloadURL: "/files/manifest"}})
|
|
return
|
|
}
|
|
if r.URL.Path != "/files/manifest" {
|
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
}
|
|
_, _ = w.Write(manifest.Content)
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, append(binaries, manifest))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(inspection.Assets) != 3 || inspection.Assets[0].State != AssetMissing || inspection.Assets[1].State != AssetMissing || inspection.Assets[2].State != AssetMatching {
|
|
t.Fatalf("inspection = %#v", inspection)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsRejectsSubsetManifest(t *testing.T) {
|
|
binaries := []ExpectedAsset{assetFor("alpha.bin", []byte("alpha")), assetFor("beta.bin", []byte("beta"))}
|
|
full, err := BuildManifest(binaries)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
subset, err := BuildManifest(binaries[:1])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
if r.URL.Path == assetListPath {
|
|
writeAssetList(t, w, []assetResponse{{ID: 3, Name: ManifestFilename, Size: subset.Size, BrowserDownloadURL: "/files/manifest"}})
|
|
return
|
|
}
|
|
_, _ = w.Write(subset.Content)
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{full})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inspection.Assets[0].State != AssetConflicting {
|
|
t.Fatalf("inspection = %#v", inspection)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsRejectsDifferentManifest(t *testing.T) {
|
|
expected, err := BuildManifest([]ExpectedAsset{assetFor("alpha.bin", []byte("alpha"))})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
different := []byte("not the declared checksum list\n")
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
if r.URL.Path == assetListPath {
|
|
writeAssetList(t, w, []assetResponse{{ID: 3, Name: ManifestFilename, Size: int64(len(different)), BrowserDownloadURL: "/files/manifest"}})
|
|
return
|
|
}
|
|
_, _ = w.Write(different)
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inspection.Assets[0].State != AssetConflicting {
|
|
t.Fatalf("inspection = %#v", inspection)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsMarksUnreadableContentUnverifiable(t *testing.T) {
|
|
expected := assetFor("app.bin", []byte("alpha"))
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
if r.URL.Path == assetListPath {
|
|
writeAssetList(t, w, []assetResponse{{ID: 2, Name: expected.Filename, Size: expected.Size, BrowserDownloadURL: "/files/app"}})
|
|
return
|
|
}
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inspection.Assets[0].State != AssetUnverifiable || inspection.Assets[0].Err == nil {
|
|
t.Fatalf("inspection = %#v", inspection)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsRejectsMalformedAttachmentList(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, assetListPath)
|
|
_, _ = w.Write([]byte("null"))
|
|
}))
|
|
defer server.Close()
|
|
_, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{assetFor("app.bin", []byte("alpha"))})
|
|
if failureKind(t, err) != FailureMalformed {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsSourceOnlyDoesNotDeclareManifest(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, assetListPath)
|
|
writeAssetList(t, w, []assetResponse{{ID: 7, Name: "historical.txt", Size: 3}})
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(inspection.Assets) != 0 || len(inspection.Unrelated) != 1 || inspection.Unrelated[0].Filename != "historical.txt" {
|
|
t.Fatalf("inspection = %#v", inspection)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsFollowsSameOriginRedirectWithAuthorization(t *testing.T) {
|
|
content := []byte("alpha")
|
|
expected := assetFor("app.bin", content)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
if r.URL.Path == assetListPath {
|
|
writeAssetList(t, w, []assetResponse{{ID: 1, Name: expected.Filename, Size: expected.Size, BrowserDownloadURL: "/files/redirect"}})
|
|
return
|
|
}
|
|
if r.URL.Path == "/files/redirect" {
|
|
http.Redirect(w, r, "/files/content", http.StatusFound)
|
|
return
|
|
}
|
|
if r.URL.Path == "/files/content" {
|
|
_, _ = w.Write(content)
|
|
return
|
|
}
|
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inspection.Assets[0].State != AssetMatching {
|
|
t.Fatalf("inspection = %#v", inspection)
|
|
}
|
|
}
|
|
|
|
func TestInspectAssetsNeverSendsTokenToCrossOriginRedirect(t *testing.T) {
|
|
var externalCalled atomic.Bool
|
|
external := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { externalCalled.Store(true) }))
|
|
defer external.Close()
|
|
expected := assetFor("app.bin", []byte("alpha"))
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assertAssetGET(t, r, "")
|
|
if r.URL.Path == assetListPath {
|
|
writeAssetList(t, w, []assetResponse{{ID: 1, Name: expected.Filename, Size: expected.Size, BrowserDownloadURL: "/files/redirect"}})
|
|
return
|
|
}
|
|
http.Redirect(w, r, external.URL+"/private", http.StatusFound)
|
|
}))
|
|
defer server.Close()
|
|
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inspection.Assets[0].State != AssetUnverifiable || externalCalled.Load() {
|
|
t.Fatalf("inspection = %#v, cross-origin called = %t", inspection, externalCalled.Load())
|
|
}
|
|
}
|
|
|
|
func assetFor(filename string, content []byte) ExpectedAsset {
|
|
sum := sha256.Sum256(content)
|
|
return ExpectedAsset{Filename: filename, SHA256: hex.EncodeToString(sum[:]), Size: int64(len(content)), Content: append([]byte(nil), content...)}
|
|
}
|
|
|
|
func writeAssetList(t *testing.T, w http.ResponseWriter, assets []assetResponse) {
|
|
t.Helper()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(assets); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func assertAssetGET(t *testing.T, r *http.Request, expectedPath string) {
|
|
t.Helper()
|
|
if r.Method != http.MethodGet {
|
|
t.Errorf("method = %q, want GET", r.Method)
|
|
}
|
|
if expectedPath != "" && r.URL.Path != expectedPath {
|
|
t.Errorf("path = %q, want %q", r.URL.Path, expectedPath)
|
|
}
|
|
if got := r.Header.Get("Authorization"); got != "token "+testToken {
|
|
t.Errorf("Authorization = %q", got)
|
|
}
|
|
}
|
|
|
|
func testDestination(base string) Destination {
|
|
return Destination{BaseURL: base, Owner: "owner", Repository: "project", Token: testToken}
|
|
}
|