// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package app import ( "context" "fmt" "net/http" "net/http/httptest" "testing" "gitea.dev/tea/internal/clierr" "github.com/stretchr/testify/require" ) // upstreamCheckout discovers the repository from the upstream remote, as in a clone of a fork. func upstreamCheckout(server *httptest.Server) *fakeGit { return &fakeGit{results: map[string]string{ "symbolic-ref --short HEAD": "main", "config --get branch.main.remote": "upstream", "remote get-url upstream": server.URL + "/alice/project.git", }} } func TestRepoDeleteAndArchiveRequireExplicitRepositoryWithYes(t *testing.T) { var requests []string server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requests = append(requests, r.Method+" "+r.URL.Path) w.WriteHeader(http.StatusNoContent) })) defer server.Close() for _, verb := range []string{"delete", "archive"} { deps, _, _ := serverDeps(t, server.URL) deps.Git = upstreamCheckout(server) err := Execute(context.Background(), deps, []string{"repo", verb, "--yes"}) require.Equal(t, clierr.Usage, clierr.ExitCode(err), "%v", err) require.ErrorContains(t, err, "--yes requires an explicit repository, not one inferred from git remote upstream: tea repo "+verb+" alice/project --yes") } require.Empty(t, requests) deps, _, _ := serverDeps(t, server.URL) deps.Git = upstreamCheckout(server) require.NoError(t, Execute(context.Background(), deps, []string{"repo", "delete", "alice/project", "--yes"})) require.Equal(t, []string{"DELETE /api/v1/repos/alice/project"}, requests) } func TestRepoDeleteAsksToTypeTheRepositoryName(t *testing.T) { var requests []string server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requests = append(requests, r.Method+" "+r.URL.Path) w.WriteHeader(http.StatusNoContent) })) defer server.Close() for _, test := range []struct { answer string deleted bool }{{"alice/other", false}, {"", false}, {"alice/project", true}} { requests = nil deps, _, _ := serverDeps(t, server.URL) deps.Git = upstreamCheckout(server) prompt := &scriptedPrompter{inputs: map[string]string{"Delete alice/project (from git remote upstream)? This cannot be undone. Type alice/project to confirm:": test.answer}} deps.Prompter = prompt deps.Streams.InTTY, deps.Streams.ErrTTY = true, true err := Execute(context.Background(), deps, []string{"repo", "delete"}) require.Len(t, prompt.asked, 1) require.Empty(t, prompt.confirmations) if !test.deleted { require.Equal(t, clierr.Canceled, clierr.ExitCode(err), "%v", err) require.Empty(t, requests) continue } require.NoError(t, err) require.Equal(t, []string{"DELETE /api/v1/repos/alice/project"}, requests) } } func TestRepoArchiveStates(t *testing.T) { for _, test := range []struct { name string args []string archived bool requests []string feedback string }{ {"archive", []string{"repo", "archive", "alice/x", "--yes"}, false, []string{"GET", "PATCH"}, "✓ Archived repository alice/x\n"}, {"already archived", []string{"repo", "archive", "alice/x", "--yes"}, true, []string{"GET"}, "! alice/x is already archived\n"}, {"unarchive", []string{"repo", "unarchive", "alice/x"}, true, []string{"GET", "PATCH"}, "✓ Unarchived repository alice/x\n"}, {"not archived", []string{"repo", "unarchive", "alice/x"}, false, []string{"GET"}, "! alice/x is not archived\n"}, } { t.Run(test.name, func(t *testing.T) { var requests []string server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "/api/v1/repos/alice/x", r.URL.Path) requests = append(requests, r.Method) archived := test.archived if r.Method == http.MethodPatch { requirePayload(t, fmt.Sprintf(`{"archived":%t}`, !archived), r.Body) archived = !archived } _, _ = fmt.Fprintf(w, `{"full_name":"alice/x","archived":%t}`, archived) })) defer server.Close() deps, out, diagnostics := repoTTYDeps(t, server.URL) require.NoError(t, Execute(context.Background(), deps, test.args)) require.Equal(t, test.requests, requests) require.Empty(t, out.String()) require.Equal(t, test.feedback, diagnostics.String()) }) } } func TestRepoArchiveAsksWithTheConsequence(t *testing.T) { server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodGet, r.Method) _, _ = w.Write([]byte(`{"full_name":"alice/project"}`)) })) defer server.Close() deps, _, _ := serverDeps(t, server.URL) deps.Git = upstreamCheckout(server) prompt := &scriptedPrompter{} deps.Prompter, deps.Streams.InTTY, deps.Streams.ErrTTY = prompt, true, true err := Execute(context.Background(), deps, []string{"repo", "archive"}) require.Equal(t, clierr.Canceled, clierr.ExitCode(err), "%v", err) require.Equal(t, []string{"Archive alice/project (from git remote upstream)? It becomes read-only."}, prompt.confirmations) } func TestRepoRenameFollowsTheRemote(t *testing.T) { server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPatch+" /api/v1/repos/alice/project", r.Method+" "+r.URL.Path) requirePayload(t, `{"name":"renamed"}`, r.Body) _, _ = w.Write([]byte(`{"name":"renamed","full_name":"alice/renamed"}`)) })) defer server.Close() deps, out, diagnostics := repoTTYDeps(t, server.URL) runner := &fakeGit{results: map[string]string{ "symbolic-ref --short HEAD": "main", "config --get branch.main.remote": "origin", "remote get-url origin": server.URL + "/alice/project.git", "remote set-url origin " + server.URL + "/alice/renamed.git": "", }} deps.Git = runner require.NoError(t, Execute(context.Background(), deps, []string{"repo", "rename", "renamed", "--yes"})) require.Contains(t, runner.calls, "remote set-url origin "+server.URL+"/alice/renamed.git") require.Empty(t, out.String()) require.Equal(t, "✓ Renamed repository alice/project to alice/renamed\n✓ Updated Git remote origin to alice/renamed\n", diagnostics.String()) } func TestRenamedURL(t *testing.T) { for _, test := range []struct{ raw, want string }{ {"https://gitea.test/alice/project.git", "https://gitea.test/alice/renamed.git"}, {"https://gitea.test/alice/project/", "https://gitea.test/alice/renamed"}, {"git@gitea-alias:alice/Project.git", "git@gitea-alias:alice/renamed.git"}, {"ssh://git@gitea.test:2222/alice/project", "ssh://git@gitea.test:2222/alice/renamed"}, {"https://gitea.test/alice/subproject.git", ""}, } { renamed, ok := renamedURL(test.raw, "project", "renamed") require.Equal(t, test.want != "", ok, test.raw) require.Equal(t, test.want, renamed, test.raw) } } func TestRepoDeleteReportsTheDeletion(t *testing.T) { server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodDelete+" /api/v1/repos/alice/x", r.Method+" "+r.URL.Path) w.WriteHeader(http.StatusNoContent) })) defer server.Close() deps, out, diagnostics := repoTTYDeps(t, server.URL) require.NoError(t, Execute(context.Background(), deps, []string{"repo", "rm", "alice/x", "--yes"})) require.Empty(t, out.String()) require.Equal(t, "✓ Deleted repository alice/x\n", diagnostics.String()) }