Merge pull request 'test(cli): admin user create and must change password value' (#3413) from earl-warren/forgejo:wip-v1.21-cli-user-create into v1.21/forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3413
Reviewed-by: twenty-panda <twenty-panda@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-04-25 09:01:08 +00:00
commit f8be8e3b21
5 changed files with 208 additions and 63 deletions

View file

@ -0,0 +1,138 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/url"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func Test_Cmd_AdminUser(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
for _, testCase := range []struct {
name string
options []string
mustChangePassword bool
}{
{
name: "default",
options: []string{},
mustChangePassword: true,
},
{
name: "--must-change-password=false",
options: []string{"--must-change-password=false"},
mustChangePassword: false,
},
{
name: "--must-change-password=true",
options: []string{"--must-change-password=true"},
mustChangePassword: true,
},
{
name: "--must-change-password",
options: []string{"--must-change-password"},
mustChangePassword: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
name := "testuser"
options := []string{"user", "create", "--username", name, "--password", "password", "--email", name + "@example.com"}
options = append(options, testCase.options...)
output, err := runMainApp("admin", options...)
assert.NoError(t, err)
assert.Contains(t, output, "has been successfully created")
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: name})
assert.Equal(t, testCase.mustChangePassword, user.MustChangePassword)
_, err = runMainApp("admin", "user", "delete", "--username", name)
assert.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{Name: name})
})
}
})
}
func Test_Cmd_AdminFirstUser(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
for _, testCase := range []struct {
name string
options []string
mustChangePassword bool
isAdmin bool
}{
{
name: "default",
options: []string{},
mustChangePassword: false,
isAdmin: false,
},
{
name: "--must-change-password=false",
options: []string{"--must-change-password=false"},
mustChangePassword: false,
isAdmin: false,
},
{
name: "--must-change-password=true",
options: []string{"--must-change-password=true"},
mustChangePassword: true,
isAdmin: false,
},
{
name: "--must-change-password",
options: []string{"--must-change-password"},
mustChangePassword: true,
isAdmin: false,
},
{
name: "--admin default",
options: []string{"--admin"},
mustChangePassword: false,
isAdmin: true,
},
{
name: "--admin --must-change-password=false",
options: []string{"--admin", "--must-change-password=false"},
mustChangePassword: false,
isAdmin: true,
},
{
name: "--admin --must-change-password=true",
options: []string{"--admin", "--must-change-password=true"},
mustChangePassword: true,
isAdmin: true,
},
{
name: "--admin --must-change-password",
options: []string{"--admin", "--must-change-password"},
mustChangePassword: true,
isAdmin: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
db.GetEngine(db.DefaultContext).Exec("DELETE FROM `user`")
db.GetEngine(db.DefaultContext).Exec("DELETE FROM `email_address`")
assert.Equal(t, int64(0), user_model.CountUsers(db.DefaultContext, nil))
name := "testuser"
options := []string{"user", "create", "--username", name, "--password", "password", "--email", name + "@example.com"}
options = append(options, testCase.options...)
output, err := runMainApp("admin", options...)
assert.NoError(t, err)
assert.Contains(t, output, "has been successfully created")
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: name})
assert.Equal(t, testCase.mustChangePassword, user.MustChangePassword)
assert.Equal(t, testCase.isAdmin, user.IsAdmin)
})
}
})
}

View file

@ -4,8 +4,11 @@ package integration
import ( import (
gocontext "context" gocontext "context"
"errors"
"io"
"net/url" "net/url"
"os" "os"
"os/exec"
"strings" "strings"
"testing" "testing"
@ -19,16 +22,18 @@ import (
func Test_CmdForgejo_Actions(t *testing.T) { func Test_CmdForgejo_Actions(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) { onGiteaRun(t, func(*testing.T, *url.URL) {
token, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-runner-token"}) token, err := runMainApp("forgejo-cli", "actions", "generate-runner-token")
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 40, len(token)) assert.EqualValues(t, 40, len(token))
secret, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-secret"}) secret, err := runMainApp("forgejo-cli", "actions", "generate-secret")
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 40, len(secret)) assert.EqualValues(t, 40, len(secret))
_, err = cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "register"}) _, err = runMainApp("forgejo-cli", "actions", "register")
assert.ErrorContains(t, err, "at least one of the --secret") var exitErr *exec.ExitError
assert.True(t, errors.As(err, &exitErr))
assert.Contains(t, string(exitErr.Stderr), "at least one of the --secret")
for _, testCase := range []struct { for _, testCase := range []struct {
testName string testName string
@ -62,10 +67,12 @@ func Test_CmdForgejo_Actions(t *testing.T) {
}, },
} { } {
t.Run(testCase.testName, func(t *testing.T) { t.Run(testCase.testName, func(t *testing.T) {
cmd := []string{"forgejo", "forgejo-cli", "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope} output, err := runMainApp("forgejo-cli", "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope)
output, err := cmdForgejoCaptureOutput(t, cmd)
assert.ErrorContains(t, err, testCase.errorMessage)
assert.EqualValues(t, "", output) assert.EqualValues(t, "", output)
var exitErr *exec.ExitError
assert.True(t, errors.As(err, &exitErr))
assert.Contains(t, string(exitErr.Stderr), testCase.errorMessage)
}) })
} }
@ -75,7 +82,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
for _, testCase := range []struct { for _, testCase := range []struct {
testName string testName string
secretOption func() string secretOption func() string
stdin []string stdin io.Reader
}{ }{
{ {
testName: "secret from argument", testName: "secret from argument",
@ -88,7 +95,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
secretOption: func() string { secretOption: func() string {
return "--secret-stdin" return "--secret-stdin"
}, },
stdin: []string{secret}, stdin: strings.NewReader(secret),
}, },
{ {
testName: "secret from file", testName: "secret from file",
@ -100,8 +107,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
}, },
} { } {
t.Run(testCase.testName, func(t *testing.T) { t.Run(testCase.testName, func(t *testing.T) {
cmd := []string{"forgejo", "forgejo-cli", "actions", "register", testCase.secretOption(), "--scope=org26"} uuid, err := runMainAppWithStdin(testCase.stdin, "forgejo-cli", "actions", "register", testCase.secretOption(), "--scope=org26")
uuid, err := cmdForgejoCaptureOutput(t, cmd, testCase.stdin...)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, expecteduuid, uuid) assert.EqualValues(t, expecteduuid, uuid)
}) })
@ -161,7 +167,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
} { } {
t.Run(testCase.testName, func(t *testing.T) { t.Run(testCase.testName, func(t *testing.T) {
cmd := []string{ cmd := []string{
"forgejo", "forgejo-cli", "actions", "register", "actions", "register",
"--secret", testCase.secret, "--scope", testCase.scope, "--secret", testCase.secret, "--scope", testCase.scope,
} }
if testCase.name != "" { if testCase.name != "" {
@ -177,7 +183,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
// Run twice to verify it is idempotent // Run twice to verify it is idempotent
// //
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
uuid, err := cmdForgejoCaptureOutput(t, cmd) uuid, err := runMainApp("forgejo-cli", cmd...)
assert.NoError(t, err) assert.NoError(t, err)
if assert.EqualValues(t, testCase.uuid, uuid) { if assert.EqualValues(t, testCase.uuid, uuid) {
ownerName, repoName, found := strings.Cut(testCase.scope, "/") ownerName, repoName, found := strings.Cut(testCase.scope, "/")

View file

@ -1,36 +0,0 @@
// SPDX-License-Identifier: MIT
package integration
import (
"bytes"
"context"
"strings"
"testing"
"code.gitea.io/gitea/cmd/forgejo"
"github.com/urfave/cli/v2"
)
func cmdForgejoCaptureOutput(t *testing.T, args []string, stdin ...string) (string, error) {
buf := new(bytes.Buffer)
app := cli.NewApp()
app.Writer = buf
app.ErrWriter = buf
ctx := context.Background()
ctx = forgejo.ContextSetNoInit(ctx, true)
ctx = forgejo.ContextSetNoExit(ctx, true)
ctx = forgejo.ContextSetStdout(ctx, buf)
ctx = forgejo.ContextSetStderr(ctx, buf)
if len(stdin) > 0 {
ctx = forgejo.ContextSetStdin(ctx, strings.NewReader(strings.Join(stdin, "")))
}
app.Commands = []*cli.Command{
forgejo.CmdForgejo(ctx),
}
err := app.Run(args)
return buf.String(), err
}

View file

@ -4,16 +4,15 @@
package integration package integration
import ( import (
"bytes" "errors"
"net/url" "net/url"
"os/exec"
"testing" "testing"
"code.gitea.io/gitea/cmd"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
) )
func Test_CmdKeys(t *testing.T) { func Test_CmdKeys(t *testing.T) {
@ -24,30 +23,30 @@ func Test_CmdKeys(t *testing.T) {
wantErr bool wantErr bool
expectedOutput string expectedOutput string
}{ }{
{"test_empty_1", []string{"keys", "--username=git", "--type=test", "--content=test"}, true, ""}, {"test_empty_1", []string{"--username=git", "--type=test", "--content=test"}, true, ""},
{"test_empty_2", []string{"keys", "-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""}, {"test_empty_2", []string{"-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""},
{ {
"with_key", "with_key",
[]string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="}, []string{"-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
false, false,
"# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost\n", "# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost\n",
}, },
{"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"}, {"invalid", []string{"--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
out := new(bytes.Buffer) out, err := runMainApp("keys", tt.args...)
app := cli.NewApp()
app.Writer = out var exitErr *exec.ExitError
app.Commands = []*cli.Command{cmd.CmdKeys} if errors.As(err, &exitErr) {
cmd.CmdKeys.HideHelp = true t.Log(string(exitErr.Stderr))
err := app.Run(append([]string{"prog"}, tt.args...)) }
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
assert.NoError(t, err) assert.NoError(t, err)
} }
assert.Equal(t, tt.expectedOutput, out.String()) assert.Equal(t, tt.expectedOutput, out)
}) })
} }
}) })

View file

@ -16,6 +16,7 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -23,6 +24,7 @@ import (
"testing" "testing"
"time" "time"
"code.gitea.io/gitea/cmd"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
@ -86,7 +88,43 @@ func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
} }
} }
// runMainApp runs the subcommand and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil, Output populates ExitError.Stderr.
func runMainApp(subcommand string, args ...string) (string, error) {
return runMainAppWithStdin(nil, subcommand, args...)
}
// runMainAppWithStdin runs the subcommand and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil, Output populates ExitError.Stderr.
func runMainAppWithStdin(stdin io.Reader, subcommand string, args ...string) (string, error) {
// running the main app directly will very likely mess with the testing setup (logger & co.)
// hence we run it as a subprocess and capture its output
args = append([]string{subcommand}, args...)
cmd := exec.Command(os.Args[0], args...)
cmd.Env = append(os.Environ(),
"GITEA_TEST_CLI=true",
"GITEA_CONF="+setting.CustomConf,
"GITEA_WORK_DIR="+setting.AppWorkPath)
cmd.Stdin = stdin
out, err := cmd.Output()
return string(out), err
}
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// GITEA_TEST_CLI is set by runMainAppWithStdin
// inspired by https://abhinavg.net/2022/05/15/hijack-testmain/
if testCLI := os.Getenv("GITEA_TEST_CLI"); testCLI == "true" {
app := cmd.NewMainApp("test-version", "integration-test")
args := append([]string{
"executable-name", // unused, but expected at position 1
"--config", os.Getenv("GITEA_CONF"),
},
os.Args[1:]..., // skip the executable name
)
if err := cmd.RunMainApp(app, args...); err != nil {
panic(err) // should never happen since RunMainApp exits on error
}
return
}
defer log.GetManager().Close() defer log.GetManager().Close()
managerCtx, cancel := context.WithCancel(context.Background()) managerCtx, cancel := context.WithCancel(context.Background())