package edit
import (
"encoding/json"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"gitcode.com/gitcode-cli/cli/api"
"gitcode.com/gitcode-cli/cli/pkg/testutil"
)
func TestEditRunWithNotesFile(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, out, _ := testutil.NewTestIOStreams()
notesPath := filepath.Join(t.TempDir(), "notes.md")
if err := os.WriteFile(notesPath, []byte("updated notes"), 0o644); err != nil {
t.Fatalf("os.WriteFile() error = %v", err)
}
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v5/repos/owner/repo/releases/tags/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"original notes"}`))
case r.Method == http.MethodPatch && r.URL.Path == "/api/v5/repos/owner/repo/releases/v1.0.0":
var opts api.GitCodeUpdateReleaseOptions
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &opts); err != nil {
t.Fatalf("Failed to unmarshal request body: %v", err)
}
if opts.Name != "v1.0.0" {
t.Errorf("Expected name 'v1.0.0', got '%s'", opts.Name)
}
if opts.Body != "updated notes" {
t.Errorf("Expected body 'updated notes', got '%s'", opts.Body)
}
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"updated notes","html_url":"https://gitcode.com/owner/repo/releases/v1.0.0"}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
NotesFile: notesPath,
})
if err != nil {
t.Fatalf("editRun() error = %v", err)
}
if !strings.Contains(out.String(), "Updated release v1.0.0") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestEditRunWithTitleOnly(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, out, _ := testutil.NewTestIOStreams()
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v5/repos/owner/repo/releases/tags/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"Old Title","body":"original notes"}`))
case r.Method == http.MethodPatch && r.URL.Path == "/api/v5/repos/owner/repo/releases/v1.0.0":
var opts api.GitCodeUpdateReleaseOptions
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &opts); err != nil {
t.Fatalf("Failed to unmarshal request body: %v", err)
}
if opts.Name != "New Title" {
t.Errorf("Expected name 'New Title', got '%s'", opts.Name)
}
if opts.Body != "original notes" {
t.Errorf("Expected body to be preserved 'original notes', got '%s'", opts.Body)
}
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"New Title","body":"original notes"}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Title: "New Title",
})
if err != nil {
t.Fatalf("editRun() error = %v", err)
}
if !strings.Contains(out.String(), "Updated release New Title") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestEditRunWithPrereleaseTrue(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, out, _ := testutil.NewTestIOStreams()
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v5/repos/owner/repo/releases/tags/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"notes"}`))
case r.Method == http.MethodPatch && r.URL.Path == "/api/v5/repos/owner/repo/releases/v1.0.0":
var opts api.GitCodeUpdateReleaseOptions
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &opts); err != nil {
t.Fatalf("Failed to unmarshal request body: %v", err)
}
if opts.ReleaseStatus != "pre" {
t.Errorf("Expected release_status 'pre', got '%s'", opts.ReleaseStatus)
}
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"notes","prerelease":true}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Prerelease: "true",
})
if err != nil {
t.Fatalf("editRun() error = %v", err)
}
if !strings.Contains(out.String(), "Updated release") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestEditRunWithPrereleaseFalse(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, out, _ := testutil.NewTestIOStreams()
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v5/repos/owner/repo/releases/tags/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"notes"}`))
case r.Method == http.MethodPatch && r.URL.Path == "/api/v5/repos/owner/repo/releases/v1.0.0":
var opts api.GitCodeUpdateReleaseOptions
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &opts); err != nil {
t.Fatalf("Failed to unmarshal request body: %v", err)
}
if opts.ReleaseStatus != "latest" {
t.Errorf("Expected release_status 'latest', got '%s'", opts.ReleaseStatus)
}
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"notes","prerelease":false}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Prerelease: "false",
})
if err != nil {
t.Fatalf("editRun() error = %v", err)
}
if !strings.Contains(out.String(), "Updated release") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestEditRunWithDraftFlagShowsWarning(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, out, errOut := testutil.NewTestIOStreams()
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v5/repos/owner/repo/releases/tags/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"notes"}`))
case r.Method == http.MethodPatch && r.URL.Path == "/api/v5/repos/owner/repo/releases/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"New Title","body":"notes"}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Title: "New Title",
Draft: "true",
})
if err != nil {
t.Fatalf("editRun() error = %v", err)
}
if !strings.Contains(errOut.String(), "warning: --draft is not supported") {
t.Fatalf("expected warning about --draft, got errOut: %q", errOut.String())
}
if !strings.Contains(out.String(), "Updated release") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestEditRunWithTargetFlagShowsWarning(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, out, errOut := testutil.NewTestIOStreams()
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v5/repos/owner/repo/releases/tags/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"v1.0.0","body":"notes"}`))
case r.Method == http.MethodPatch && r.URL.Path == "/api/v5/repos/owner/repo/releases/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"v1.0.0","name":"New Title","body":"notes"}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Title: "New Title",
Target: "main",
})
if err != nil {
t.Fatalf("editRun() error = %v", err)
}
if !strings.Contains(errOut.String(), "warning: --target is not supported") {
t.Fatalf("expected warning about --target, got errOut: %q", errOut.String())
}
if !strings.Contains(out.String(), "Updated release") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestEditRunWithSlashTag(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, out, _ := testutil.NewTestIOStreams()
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v5/repos/owner/repo/releases/tags/release/v1.0.0":
_, _ = w.Write([]byte(`{"tag_name":"release/v1.0.0","name":"release/v1.0.0","body":"notes"}`))
case r.Method == http.MethodPatch && r.URL.Path == "/api/v5/repos/owner/repo/releases/release/v1.0.0":
rawURL := r.URL.String()
if !strings.Contains(rawURL, "release%2Fv1.0.0") {
expectedEscaped := "release%2Fv1.0.0"
t.Errorf("Expected escaped tag '%s' in URL, got %s", expectedEscaped, rawURL)
}
_, _ = w.Write([]byte(`{"tag_name":"release/v1.0.0","name":"New Title","body":"notes"}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "release/v1.0.0",
Title: "New Title",
})
if err != nil {
t.Fatalf("editRun() error = %v", err)
}
if !strings.Contains(out.String(), "Updated release") {
t.Fatalf("unexpected output: %q", out.String())
}
}
func TestEditRunWithNoChanges(t *testing.T) {
t.Setenv("GC_TOKEN", "test-token")
streams, _, _, _ := testutil.NewTestIOStreams()
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return nil, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
})
if err == nil {
t.Fatal("Expected error for no changes")
}
if !strings.Contains(err.Error(), "no changes specified") {
t.Fatalf("Expected 'no changes specified' error, got: %v", err)
}
}
func TestEditRunScansInlineNotesForSecrets(t *testing.T) {
t.Setenv("GC_TOKEN", "secret-token-abc123")
streams, _, _, _ := testutil.NewTestIOStreams()
client := testutil.NewTestHTTPClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unexpected request: %s %s (scan should reject before any API call)", r.Method, r.URL.Path)
}))
err := editRun(&EditOptions{
IO: streams,
HttpClient: func() (*http.Client, error) { return client, nil },
Repository: "owner/repo",
TagName: "v1.0.0",
Notes: "leaked: secret-token-abc123",
})
if err == nil || !strings.Contains(err.Error(), "secret") {
t.Fatalf("editRun() error = %v, want secret detection error", err)
}
}