package upload
import (
"encoding/json"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"gitcode.com/gitcode-cli/cli/pkg/iostreams"
"strings"
"testing"
cmdutil "gitcode.com/gitcode-cli/cli/pkg/cmdutil"
"gitcode.com/gitcode-cli/cli/pkg/testutil"
)
func TestUploadRun(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
io, _, out, _ := testutil.NewTestIOStreams()
filePath := filepath.Join(t.TempDir(), "asset.txt")
if err := os.WriteFile(filePath, []byte("hello"), 0o644); err != nil {
t.Fatalf("os.WriteFile() error = %v", err)
}
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/tags/v1.0.0"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","assets":[]}`))
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/v1.0.0/upload_url"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"url":"https://uploads.gitcode.test/upload/asset.txt","headers":{"X-Test":"1"}}`))
case r.Method == http.MethodPut && r.URL.Path == "/upload/asset.txt":
w.WriteHeader(http.StatusCreated)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := uploadRun(&UploadOptions{
IO: io,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Files: []string{filePath},
})
if err != nil {
t.Fatalf("uploadRun() error = %v", err)
}
if !strings.Contains(out.String(), "Uploaded asset.txt") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestUploadRunJSONWritesUploadedFiles(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
io, _, out, _ := testutil.NewTestIOStreams()
filePath := filepath.Join(t.TempDir(), "asset.txt")
if err := os.WriteFile(filePath, []byte("hello"), 0o644); err != nil {
t.Fatalf("os.WriteFile() error = %v", err)
}
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/tags/v1.0.0"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","assets":[]}`))
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/v1.0.0/upload_url"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"url":"https://uploads.gitcode.test/upload/asset.txt","headers":{"X-Test":"1"}}`))
case r.Method == http.MethodPut && r.URL.Path == "/upload/asset.txt":
w.WriteHeader(http.StatusCreated)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := uploadRun(&UploadOptions{
IO: io,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Files: []string{filePath},
JSON: true,
})
if err != nil {
t.Fatalf("uploadRun() error = %v", err)
}
var got []map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
t.Fatalf("JSON output did not parse: %v\n%s", err, out.String())
}
if len(got) != 1 || got[0]["name"] != "asset.txt" || got[0]["size"] != float64(5) || got[0]["content_type"] != "text/plain" {
t.Fatalf("JSON output = %#v", got)
}
if strings.Contains(out.String(), "Uploaded asset.txt") {
t.Fatalf("JSON output contains text banner: %q", out.String())
}
}
func TestUploadRunRejectsUnsupportedLabel(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
io, _, _, _ := testutil.NewTestIOStreams()
err := uploadRun(&UploadOptions{
IO: io,
HttpClient: func() (*http.Client, error) { return nil, errors.New("should not be called") },
Repository: "owner/repo",
TagName: "v1.0.0",
Files: []string{"asset.txt"},
Label: "linux-amd64",
})
if err == nil {
t.Fatal("uploadRun() error = nil, want usage error")
}
if got := cmdutil.ExitCode(err); got != cmdutil.ExitUsage {
t.Fatalf("ExitCode() = %d, want %d", got, cmdutil.ExitUsage)
}
}
func TestUploadRunRefusesExistingAssetNames(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
ioStreams, _, _, _ := iostreams.Test()
filePath := filepath.Join(t.TempDir(), "asset.txt")
if err := os.WriteFile(filePath, []byte("hello"), 0o644); err != nil {
t.Fatalf("os.WriteFile() error = %v", err)
}
putCount := 0
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/tags/v1.0.0"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","assets":[{"name":"asset.txt"}]}`))
case r.Method == http.MethodPut:
putCount++
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := uploadRun(&UploadOptions{
IO: ioStreams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Files: []string{filePath},
})
if err == nil || !strings.Contains(err.Error(), "already has asset(s): asset.txt") || !strings.Contains(err.Error(), "delete-asset") {
t.Fatalf("error = %v, want same-name refusal with delete-asset guidance", err)
}
if putCount != 0 {
t.Errorf("PUT count = %d, want 0 (refuse before any upload)", putCount)
}
}
func TestUploadRunNamesPartialSuccessOnFailure(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
ioStreams, _, _, _ := iostreams.Test()
dir := t.TempDir()
first := filepath.Join(dir, "a.txt")
second := filepath.Join(dir, "b.txt")
for _, f := range []string{first, second} {
if err := os.WriteFile(f, []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
}
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/tags/v1.0.0"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","assets":[]}`))
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/v1.0.0/upload_url"):
if r.URL.Query().Get("file_name") == "b.txt" {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"url":"https://uploads.gitcode.test/upload/a.txt","headers":{"X-Test":"1"}}`))
case r.Method == http.MethodPut && r.URL.Path == "/upload/a.txt":
w.WriteHeader(http.StatusCreated)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := uploadRun(&UploadOptions{
IO: ioStreams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Files: []string{first, second},
})
if err == nil || !strings.Contains(err.Error(), "already uploaded in this run: a.txt") {
t.Fatalf("error = %v, want partial-success list in the error", err)
}
}
func TestUploadRunStreamsFileContentIntact(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
ioStreams, _, _, _ := iostreams.Test()
dir := t.TempDir()
filePath := filepath.Join(dir, "payload.bin")
payload := strings.Repeat("x", 4096)
if err := os.WriteFile(filePath, []byte(payload), 0o644); err != nil {
t.Fatal(err)
}
var putBody string
var putLength int64
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/tags/v1.0.0"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","assets":[]}`))
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/v1.0.0/upload_url"):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"url":"https://uploads.gitcode.test/upload/payload.bin","headers":{}}`))
case r.Method == http.MethodPut:
body, _ := io.ReadAll(r.Body)
putBody = string(body)
putLength = r.ContentLength
w.WriteHeader(http.StatusCreated)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := uploadRun(&UploadOptions{
IO: ioStreams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Files: []string{filePath},
})
if err != nil {
t.Fatalf("uploadRun() error = %v", err)
}
if putBody != payload {
t.Errorf("uploaded body length = %d, want %d (streaming must not truncate)", len(putBody), len(payload))
}
if putLength != int64(len(payload)) {
t.Errorf("Content-Length = %d, want %d", putLength, int64(len(payload)))
}
}
func TestUploadRunReleaseNotFoundIsWrapped(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
ioStreams, _, _, _ := iostreams.Test()
filePath := filepath.Join(t.TempDir(), "asset.txt")
if err := os.WriteFile(filePath, []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
err := uploadRun(&UploadOptions{
IO: ioStreams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v9.9.9",
Files: []string{filePath},
})
if err == nil || !strings.Contains(err.Error(), "release v9.9.9 not found in owner/repo") {
t.Fatalf("error = %v, want wrapped not-found", err)
}
if code := cmdutil.ExitCode(err); code != 3 {
t.Errorf("exit code = %d, want 3", code)
}
}
func TestUploadRunRefusesDirectoryAndBatchDuplicates(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
ioStreams, _, _, _ := iostreams.Test()
dir := t.TempDir()
inner := filepath.Join(dir, "sub")
if err := os.MkdirAll(inner, 0o755); err != nil {
t.Fatal(err)
}
twinA := filepath.Join(dir, "a", "tool.txt")
twinB := filepath.Join(dir, "b", "tool.txt")
for _, f := range []string{twinA, twinB} {
if err := os.MkdirAll(filepath.Dir(f), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(f, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
}
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v5/repos/owner/repo/releases/tags/v1.0.0") {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","assets":[]}`))
return
}
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}))
options := func(files ...string) *UploadOptions {
return &UploadOptions{
IO: ioStreams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Files: files,
}
}
if err := uploadRun(options(inner)); err == nil || !strings.Contains(err.Error(), "cannot upload a directory") {
t.Errorf("directory error = %v, want cannot-upload-a-directory", err)
}
if err := uploadRun(options(twinA, twinB)); err == nil ||
!strings.Contains(err.Error(), "tool.txt (twice in this batch)") {
t.Errorf("batch duplicate error = %v, want batch-duplicate refusal", err)
}
}