Upload missing Gitea release assets
This commit is contained in:
323
internal/gitea/upload_test.go
Normal file
323
internal/gitea/upload_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUploadMissingPartialFailureThenRerunOnlyMissingAssets(t *testing.T) {
|
||||
fixture := &uploadFixture{files: make(map[string][]byte), failName: "beta.bin"}
|
||||
server := httptest.NewServer(fixture)
|
||||
defer server.Close()
|
||||
client := testClient(server)
|
||||
destination := testDestination(server.URL)
|
||||
assets := localAssets(t, []string{"zeta.bin", "alpha.bin", "beta.bin"})
|
||||
manifest, err := BuildManifest(assets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assets = append(assets, manifest)
|
||||
|
||||
initial, err := client.InspectAssets(context.Background(), destination, 10, assets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := client.UploadMissing(context.Background(), destination, 10, initial)
|
||||
if err == nil || first.State != DeliveryPartial {
|
||||
t.Fatalf("first upload result = %#v, error = %v", first, err)
|
||||
}
|
||||
if got, want := names(first.Uploaded), []string{"alpha.bin"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("uploaded = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := names(first.Pending), []string{"zeta.bin", "beta.bin", "SHA256SUMS"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pending = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
secondInspection, err := client.InspectAssets(context.Background(), destination, 10, assets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := client.UploadMissing(context.Background(), destination, 10, secondInspection)
|
||||
if err != nil || second.State != DeliveryComplete {
|
||||
t.Fatalf("second upload result = %#v, error = %v", second, err)
|
||||
}
|
||||
if got, want := names(second.Uploaded), []string{"beta.bin", "zeta.bin", "SHA256SUMS"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("second uploaded = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := fixture.postNames(), []string{"alpha.bin", "beta.bin", "beta.bin", "zeta.bin", "SHA256SUMS"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("POST order = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadMissingAcceptsConcurrentMatchingAttachmentWithoutReplacement(t *testing.T) {
|
||||
fixture := &uploadFixture{files: make(map[string][]byte), conflictName: "app.bin"}
|
||||
server := httptest.NewServer(fixture)
|
||||
defer server.Close()
|
||||
client := testClient(server)
|
||||
destination := testDestination(server.URL)
|
||||
assets := localAssets(t, []string{"app.bin"})
|
||||
initial, err := client.InspectAssets(context.Background(), destination, 10, assets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.UploadMissing(context.Background(), destination, 10, initial)
|
||||
if err != nil || result.State != DeliveryComplete {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
if got, want := names(result.Matching), []string{"app.bin"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("matching = %v, want %v", got, want)
|
||||
}
|
||||
if len(result.Uploaded) != 0 || len(fixture.postNames()) != 1 {
|
||||
t.Fatalf("result = %#v, POST names = %v", result, fixture.postNames())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadMissingReconcilesLostManifestAcknowledgment(t *testing.T) {
|
||||
assets := localAssets(t, []string{"app.bin"})
|
||||
manifest, err := BuildManifest(assets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fixture := &uploadFixture{files: make(map[string][]byte), dropAckName: ManifestFilename}
|
||||
// The local binary is represented by its file path; the server starts with
|
||||
// that exact content so only the checksum manifest needs uploading.
|
||||
fixture.files["app.bin"] = readExpectedFile(t, assets[0])
|
||||
server := httptest.NewServer(fixture)
|
||||
defer server.Close()
|
||||
client := testClient(server)
|
||||
destination := testDestination(server.URL)
|
||||
expected := append(assets, manifest)
|
||||
initial, err := client.InspectAssets(context.Background(), destination, 10, expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.UploadMissing(context.Background(), destination, 10, initial)
|
||||
if err != nil || result.State != DeliveryComplete {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
if got, want := names(result.Matching), []string{"app.bin", ManifestFilename}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("matching = %v, want %v", got, want)
|
||||
}
|
||||
if len(result.Uploaded) != 0 || !reflect.DeepEqual(fixture.postNames(), []string{ManifestFilename}) {
|
||||
t.Fatalf("result = %#v, POST names = %v", result, fixture.postNames())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadMissingReportsUnknownWhenReconciliationReadFails(t *testing.T) {
|
||||
fixture := &uploadFixture{files: make(map[string][]byte), dropAckName: "app.bin", failListAfter: 1}
|
||||
server := httptest.NewServer(fixture)
|
||||
defer server.Close()
|
||||
client := testClient(server)
|
||||
destination := testDestination(server.URL)
|
||||
assets := localAssets(t, []string{"app.bin"})
|
||||
initial, err := client.InspectAssets(context.Background(), destination, 10, assets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.UploadMissing(context.Background(), destination, 10, initial)
|
||||
if err == nil || result.State != DeliveryUnknown {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
if got, want := names(result.Pending), []string{"app.bin"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pending = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := names(result.Uncertain), []string{"app.bin"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("uncertain = %v, want %v", got, want)
|
||||
}
|
||||
if got := fixture.postNames(); !reflect.DeepEqual(got, []string{"app.bin"}) {
|
||||
t.Fatalf("POST names = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadMissingPreflightConflictPreventsEveryWrite(t *testing.T) {
|
||||
fixture := &uploadFixture{files: map[string][]byte{"app.bin": []byte("different bytes")}}
|
||||
server := httptest.NewServer(fixture)
|
||||
defer server.Close()
|
||||
client := testClient(server)
|
||||
destination := testDestination(server.URL)
|
||||
assets := localAssets(t, []string{"app.bin", "other.bin"})
|
||||
inspection, err := client.InspectAssets(context.Background(), destination, 10, assets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.UploadMissing(context.Background(), destination, 10, inspection)
|
||||
if err == nil || result.State != DeliveryConflict {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
if len(fixture.postNames()) != 0 {
|
||||
t.Fatalf("uploads happened before conflict review: %v", fixture.postNames())
|
||||
}
|
||||
if got, want := names(result.Conflicting), []string{"app.bin"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("conflicting = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := names(result.Pending), []string{"other.bin"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pending = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type uploadFixture struct {
|
||||
mu sync.Mutex
|
||||
files map[string][]byte
|
||||
posts []string
|
||||
failName string
|
||||
conflictName string
|
||||
dropAckName string
|
||||
failListAfter int
|
||||
listCount int
|
||||
}
|
||||
|
||||
func (fixture *uploadFixture) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "token "+testToken {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && r.URL.Path == assetListPath {
|
||||
fixture.mu.Lock()
|
||||
fixture.listCount++
|
||||
if fixture.failListAfter > 0 && fixture.listCount > fixture.failListAfter {
|
||||
fixture.mu.Unlock()
|
||||
http.Error(w, "read failed", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
assets := make([]assetResponse, 0, len(fixture.files))
|
||||
for name, content := range fixture.files {
|
||||
assets = append(assets, assetResponse{ID: int64(len(assets) + 1), Name: name, Size: int64(len(content)), BrowserDownloadURL: "/files/" + urlPathEscape(name)})
|
||||
}
|
||||
fixture.mu.Unlock()
|
||||
_ = json.NewEncoder(w).Encode(assets)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/files/") {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/files/")
|
||||
fixture.mu.Lock()
|
||||
content, ok := fixture.files[name]
|
||||
fixture.mu.Unlock()
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(content)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost && r.URL.Path == assetListPath {
|
||||
fixture.receiveUpload(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "unexpected request", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
func (fixture *uploadFixture) receiveUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data; boundary=") {
|
||||
http.Error(w, "multipart required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
http.Error(w, "invalid multipart body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("attachment")
|
||||
if err != nil {
|
||||
http.Error(w, "missing attachment field", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, "read multipart file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if header.Filename != r.URL.Query().Get("name") {
|
||||
http.Error(w, "filename query mismatch", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := header.Filename
|
||||
fixture.mu.Lock()
|
||||
fixture.posts = append(fixture.posts, name)
|
||||
attempts := 0
|
||||
for _, posted := range fixture.posts {
|
||||
if posted == name {
|
||||
attempts++
|
||||
}
|
||||
}
|
||||
if name == fixture.failName && attempts == 1 {
|
||||
fixture.mu.Unlock()
|
||||
http.Error(w, "write failed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fixture.files[name] = append([]byte(nil), content...)
|
||||
fixture.mu.Unlock()
|
||||
if name == fixture.conflictName {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if name == fixture.dropAckName {
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "hijacking unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
connection, _, err := hijacker.Hijack()
|
||||
if err == nil {
|
||||
_ = connection.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(assetResponse{ID: 50, Name: name, Size: int64(len(content))})
|
||||
}
|
||||
|
||||
func localAssets(t *testing.T, names []string) []ExpectedAsset {
|
||||
t.Helper()
|
||||
output := t.TempDir()
|
||||
assets := make([]ExpectedAsset, 0, len(names))
|
||||
for _, name := range names {
|
||||
content := []byte("binary-" + name)
|
||||
path := filepath.Join(output, name)
|
||||
if err := os.WriteFile(path, content, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := assetFor(name, content)
|
||||
sum.Content = nil
|
||||
sum.Path = path
|
||||
assets = append(assets, sum)
|
||||
}
|
||||
return assets
|
||||
}
|
||||
|
||||
func readExpectedFile(t *testing.T, asset ExpectedAsset) []byte {
|
||||
t.Helper()
|
||||
content, err := os.ReadFile(asset.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (fixture *uploadFixture) postNames() []string {
|
||||
fixture.mu.Lock()
|
||||
defer fixture.mu.Unlock()
|
||||
return append([]string(nil), fixture.posts...)
|
||||
}
|
||||
|
||||
func names(identities []AssetIdentity) []string {
|
||||
result := make([]string, 0, len(identities))
|
||||
for _, identity := range identities {
|
||||
result = append(result, identity.Filename)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func urlPathEscape(value string) string {
|
||||
return (&url.URL{Path: value}).EscapedPath()
|
||||
}
|
||||
Reference in New Issue
Block a user