Files
gitea-tea/internal/app/repo_mutations_test.go
T
bircni 7cd13ef64e feat(repo): add repo unarchive and repo rename, and report archive and delete
repo unarchive restores an archived repository, and repo rename renames
one, moving the Git remote or the saved default repository that selected
it to the new name. The archived-repository hint now points to
tea repo unarchive.

repo archive asks "Archive alice/x? It becomes read-only." and warns
"! alice/x is already archived" without a request when there is nothing
to do. Archive, unarchive, rename and delete confirm with a ✓ line on a
terminal, and --json returns the repository.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:51:45 +02:00

173 lines
7.4 KiB
Go

// 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())
}