Files
gitea-tea/internal/app/actions_test.go
T
bircni 1f4b5f868f refactor(prompts): fold Asker into Prompter and share delete confirmations
FormPrompter is the only production prompter and implements the rich
prompt API, so Ask, AskText and ConfirmDefault become the Prompter
interface and the plain Input/Confirm fallbacks go away.

Confirmations that default to no derive their non-terminal wording
from the prompt, and single deletes share deleteConfirmed for the
prompt, the request and the done line.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-12 10:47:54 +02:00

242 lines
11 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"context"
"encoding/base64"
"encoding/json"
"strings"
"testing"
"gitea.dev/tea/internal/clierr"
"github.com/stretchr/testify/require"
)
const ciWorkflowFile = `name: CI
on:
push:
workflow_dispatch:
inputs:
target:
description: Deploy target
required: true
default: staging
type: choice
options: [staging, production]
dry_run:
type: boolean
default: true
note:
description: Free-form note
required: true
jobs: {}
`
// contentsOf is the contents API response for a file.
func contentsOf(content string) string {
data, _ := json.Marshal(map[string]string{"type": "file", "encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(content))})
return string(data)
}
func TestWorkflowListAndView(t *testing.T) {
server, _ := actionsAPI(t, map[string]string{
"GET actions/workflows": workflowsFixture,
"GET actions/workflows/ci.yml/runs": `{"total_count":1,"workflow_runs":[` + queuedRunFixture + `]}`,
"GET contents/.gitea/workflows/ci.yml": contentsOf(ciWorkflowFile),
})
for _, tc := range []struct {
name string
tty bool
args []string
want string
}{
{"list", true, []string{"list"}, "Showing 2 of 2 workflows in alice/project\n\nNAME FILE STATE\nCI ci.yml active\nNightly nightly.yaml disabled\n"},
{"list piped", false, []string{"list"}, "CI\tci.yml\tactive\t.gitea/workflows/ci.yml\thttps://gitea.test/alice/project/src/branch/main/.gitea/workflows/ci.yml\n" +
"Nightly\tnightly.yaml\tdisabled\t.gitea/workflows/nightly.yaml\t\n"},
{"view by name", true, []string{"view", "ci"}, `CI ci.yml
active • .gitea/workflows/ci.yml
RECENT RUNS
STATUS ID WORKFLOW TITLE BRANCH EVENT ELAPSED AGE
* 47 CI Add workflows main workflow_dispatch about 2 minutes ago
To see more runs: tea run list --workflow ci.yml -R alice/project
View this workflow on Gitea: https://gitea.test/alice/project/src/branch/main/.gitea/workflows/ci.yml
`},
{"view the file", false, []string{"view", ".gitea/workflows/ci.yml", "--yaml"}, ciWorkflowFile},
{"view as JSON", false, []string{"view", "CI", "--json", "id,state"}, `{"id":"ci.yml","state":"active"}`},
} {
t.Run(tc.name, func(t *testing.T) {
deps, out, _ := actionsDeps(t, server.URL, tc.tty)
require.NoError(t, Execute(context.Background(), deps, append(append([]string{"workflow"}, tc.args...), "-R", "alice/project")))
if strings.HasPrefix(tc.want, "{") {
require.JSONEq(t, tc.want, out.String())
} else {
require.Equal(t, tc.want, out.String())
}
})
}
deps, _, _ := actionsDeps(t, server.URL, false)
require.EqualError(t, Execute(context.Background(), deps, []string{"workflow", "view", "deploy", "-R", "alice/project"}), `workflow "deploy" not found in alice/project`)
}
func TestWorkflowRunChecksInputsBeforeDispatching(t *testing.T) {
server, requests := actionsAPI(t, map[string]string{
"GET actions/workflows": workflowsFixture,
"GET contents/.gitea/workflows/ci.yml": contentsOf(ciWorkflowFile),
})
for _, tc := range []struct {
args []string
err string
}{
{[]string{"-f", "note=x", "-f", "bogus=1"}, `unknown input "bogus" for CI (ci.yml); inputs: target, dry_run, note`},
{[]string{"-f", "note=x", "-f", "target=moon"}, `invalid value "moon" for input target: use staging, production`},
{[]string{"-f", "note=x", "-f", "dry_run=maybe"}, `invalid value "maybe" for input dry_run: use true or false`},
{[]string{"-f", "target=production"}, "missing required input note for CI (ci.yml); pass -f note=<value>"},
{nil, "missing required input note for CI (ci.yml); pass -f note=<value>"},
} {
*requests = nil
deps, _, _ := actionsDeps(t, server.URL, false)
err := Execute(context.Background(), deps, append([]string{"workflow", "run", "CI", "-R", "alice/project", "--ref", "main"}, tc.args...))
require.Equal(t, clierr.Usage, clierr.ExitCode(err), "%v", tc.args)
require.EqualError(t, err, tc.err)
require.Equal(t, []string{"GET actions/workflows", "GET contents/.gitea/workflows/ci.yml"}, *requests, "nothing is dispatched")
}
deps, _, _ := actionsDeps(t, server.URL, false)
require.EqualError(t, Execute(context.Background(), deps, []string{"workflow", "run", "nightly", "-R", "alice/project"}),
"workflow nightly.yaml is disabled; enable it with: tea workflow enable nightly.yaml -R alice/project")
}
// dispatchingPrompter answers workflow inputs: selections with their first choice, confirmations
// with yes, and text by title.
type dispatchingPrompter struct {
choosingPrompter
asked []string
}
func (p *dispatchingPrompter) Ask(_ context.Context, title string, validate func(string) error) (string, error) {
p.asked = append(p.asked, title)
answer := p.inputs[title]
return answer, validate(answer)
}
func (p *dispatchingPrompter) AskText(context.Context, string, string) (string, error) {
return "", nil
}
func (p *dispatchingPrompter) ConfirmDefault(_ context.Context, title string, defaultYes bool) (bool, error) {
p.asked = append(p.asked, title)
return !defaultYes, nil
}
func TestWorkflowRunDispatchesOnTheDefaultBranch(t *testing.T) {
server, requests := actionsAPI(t, map[string]string{
"GET ": `{"full_name":"alice/project","default_branch":"trunk"}`,
"GET actions/workflows": workflowsFixture,
"GET contents/.gitea/workflows/ci.yml": contentsOf(ciWorkflowFile),
"POST actions/workflows/ci.yml/dispatches": `{"workflow_run_id":51,"html_url":"https://gitea.test/alice/project/actions/runs/51"}`,
})
for _, tc := range []struct {
name, out, err string
args []string
prompter *dispatchingPrompter
payload string
}{
{
"flags", "https://gitea.test/alice/project/actions/runs/51\n", "✓ Dispatched CI (ci.yml) on trunk → run 51\n Watch it: tea run watch 51 -R alice/project\n",
[]string{"-f", "note=ship", "-f", "dry_run=FALSE"},
nil, `{"ref":"trunk","inputs":{"dry_run":"false","note":"ship"}}`,
},
{
"prompts", "https://gitea.test/alice/project/actions/runs/51\n", "✓ Dispatched CI (ci.yml) on trunk → run 51\n Watch it: tea run watch 51 -R alice/project\n",
nil, &dispatchingPrompter{choosingPrompter: choosingPrompter{scriptedPrompter: scriptedPrompter{inputs: map[string]string{"note — Free-form note": "ship"}}}},
`{"ref":"trunk","inputs":{"dry_run":"false","note":"ship","target":"staging"}}`,
},
} {
t.Run(tc.name, func(t *testing.T) {
*requests = nil
deps, out, diagnostics := actionsDeps(t, server.URL, true)
if tc.prompter != nil {
deps.Prompter = tc.prompter
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
}
require.NoError(t, Execute(context.Background(), deps, append([]string{"workflow", "run", "ci.yml", "-R", "alice/project"}, tc.args...)))
require.Equal(t, tc.out, out.String())
require.Equal(t, tc.err, diagnostics.String())
require.Equal(t, []string{"GET actions/workflows", "GET ", "GET contents/.gitea/workflows/ci.yml", "POST actions/workflows/ci.yml/dispatches " + tc.payload}, *requests)
if tc.prompter != nil {
require.Equal(t, []string{"target — Deploy target"}, tc.prompter.titles)
require.Equal(t, []string{"dry_run", "note — Free-form note"}, tc.prompter.asked)
}
})
}
}
func TestWorkflowRunNeedsTheCheckedOutBranchOnTheServer(t *testing.T) {
server, requests := actionsAPI(t, map[string]string{
"GET ": `{"full_name":"alice/project","default_branch":"main"}`,
"GET actions/workflows": workflowsFixture,
"GET branches/local-only": `404 {"message":"branch not found"}`,
})
deps, _, _ := actionsDeps(t, server.URL, false)
deps.Git = &fakeGit{results: map[string]string{
"remote": "origin", "remote get-url origin": server.URL + "/alice/project.git",
"symbolic-ref --short HEAD": "local-only", "config --get branch.local-only.remote": "origin",
}}
err := Execute(context.Background(), deps, []string{"workflow", "run", "ci.yml", "-f", "note=x"})
require.EqualError(t, err, "branch local-only is not on alice/project; push it or pass --ref main")
require.Equal(t, []string{"GET actions/workflows", "GET branches/local-only", "GET "}, *requests)
}
func TestWorkflowStateChangesNeedNoConfirmation(t *testing.T) {
server, requests := actionsAPI(t, map[string]string{
"GET actions/workflows": workflowsFixture,
"PUT actions/workflows/nightly.yaml/enable": "204 ",
"GET actions/workflows/nightly.yaml": `{"id":"nightly.yaml","name":"Nightly","path":".gitea/workflows/nightly.yaml","state":"active"}`,
})
for _, tc := range []struct {
args []string
out, err string
requests []string
}{
{[]string{"enable", "Nightly"}, "", "✓ Enabled Nightly (nightly.yaml)\n", []string{"GET actions/workflows", "PUT actions/workflows/nightly.yaml/enable", "GET actions/workflows/nightly.yaml"}},
{[]string{"enable", "nightly", "--json", "state"}, `{"state":"active"}`, "", []string{"GET actions/workflows", "PUT actions/workflows/nightly.yaml/enable", "GET actions/workflows/nightly.yaml"}},
{[]string{"disable", "NIGHTLY"}, "", "! Nightly (nightly.yaml) is already disabled\n", []string{"GET actions/workflows"}},
} {
*requests = nil
deps, out, diagnostics := actionsDeps(t, server.URL, true)
prompter := &scriptedPrompter{}
deps.Prompter = prompter
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
require.NoError(t, Execute(context.Background(), deps, append(append([]string{"workflow"}, tc.args...), "-R", "alice/project")))
if tc.out != "" {
require.JSONEq(t, tc.out, out.String())
} else {
require.Empty(t, out.String())
}
require.Equal(t, tc.err, diagnostics.String())
require.Equal(t, tc.requests, *requests)
require.Empty(t, prompter.confirmations)
}
}
func TestWorkflowRunFollowStreamsTheLogsOfItsJobs(t *testing.T) {
server, _ := actionsAPI(t, map[string]string{
"GET actions/workflows": workflowsFixture,
"GET contents/.gitea/workflows/ci.yml": contentsOf("on: workflow_dispatch\n"),
"POST actions/workflows/ci.yml/dispatches": `{"workflow_run_id":11,"html_url":"https://gitea.test/alice/project/actions/runs/11"}`,
"GET actions/runs/11/jobs": failedJobsFixture,
"GET actions/jobs/30/logs": "compiled\n",
"GET actions/jobs/31/logs": "boom\n",
})
deps, out, diagnostics := actionsDeps(t, server.URL, false)
require.NoError(t, Execute(context.Background(), deps, []string{"workflow", "run", "ci", "-R", "alice/project", "--ref", "main", "--follow", "--interval=1ms"}))
require.Equal(t, "https://gitea.test/alice/project/actions/runs/11\nbuild\tcompiled\ntest\tboom\n", out.String())
require.Equal(t, "build (job 30)\ntest (job 31)\n", diagnostics.String())
err := Execute(context.Background(), deps, []string{"workflow", "run", "ci.yml", "-R", "alice/project", "--ref", "main", "--follow", "--json"})
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
require.ErrorContains(t, err, "--follow cannot be combined with --json")
}