mirror of
https://gitea.com/gitea/tea.git
synced 2026-05-15 20:29:22 +02:00
feat(ssh-keys): add ssh-keys command to manage SSH public keys (#940)
## Summary - Adds `tea ssh-keys` command group (aliases: `ssh-key`, `keys`) under the SETUP category - Mirrors the interface of `gh ssh-key add/list/delete` - Three subcommands: `add <keyfile>`, `list`, `delete <id>` ## Commands \`\`\`sh tea ssh-keys add ~/.ssh/id_ed25519.pub # title defaults to filename stem tea ssh-keys add ~/.ssh/id_rsa.pub --title "work laptop" tea ssh-keys add ~/.ssh/deploy.pub --read-only # authentication-only key tea ssh-keys list tea ssh-keys list --output json tea ssh-keys delete 42 # prompts for confirmation tea ssh-keys delete 42 --force # skip prompt \`\`\` ## Test plan - [x] `make lint` — 0 issues - [x] `make fmt-check` — passes - [x] `go test ./cmd/sshkeys/... -run TestKeyTitle` — unit tests pass (no server needed) - [ ] Integration tests with live Gitea instance: \`\`\`sh GITEA_TEA_TEST_URL=https://your-gitea \ GITEA_TEA_TEST_TOKEN=<token> \ go test ./cmd/sshkeys/... -v -run TestSSHKey \`\`\` Exercises full add → SDK-verify → delete → 404-verify lifecycle. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Brandon Fryslie <530235+brandon-fryslie@users.noreply.github.com> Reviewed-on: https://gitea.com/gitea/tea/pulls/940 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Brandon Fryslie <186614+brandroid@noreply.gitea.com> Co-committed-by: Brandon Fryslie <186614+brandroid@noreply.gitea.com>
This commit is contained in:
committed by
Lunny Xiao
parent
2985824ab0
commit
9d6ae4bf02
@@ -45,6 +45,8 @@ func App() *cli.Command {
|
||||
&CmdNotifications,
|
||||
&CmdRepoClone,
|
||||
|
||||
&CmdSSHKeys,
|
||||
|
||||
&CmdAdmin,
|
||||
|
||||
&CmdApi,
|
||||
|
||||
32
cmd/sshkeys.go
Normal file
32
cmd/sshkeys.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
|
||||
"code.gitea.io/tea/cmd/sshkeys"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdSSHKeys represents the ssh-keys command group
|
||||
var CmdSSHKeys = cli.Command{
|
||||
Name: "ssh-keys",
|
||||
Aliases: []string{"ssh-key"},
|
||||
Category: catSetup,
|
||||
Usage: "Manage SSH public keys",
|
||||
Description: "List, add, or delete SSH public keys on the current user's account",
|
||||
ArgsUsage: " ",
|
||||
Action: runSSHKeys,
|
||||
Commands: []*cli.Command{
|
||||
&sshkeys.CmdSSHKeyList,
|
||||
&sshkeys.CmdSSHKeyAdd,
|
||||
&sshkeys.CmdSSHKeyDelete,
|
||||
},
|
||||
Flags: sshkeys.CmdSSHKeyList.Flags,
|
||||
}
|
||||
|
||||
func runSSHKeys(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
return sshkeys.RunSSHKeyList(ctx, cmd)
|
||||
}
|
||||
74
cmd/sshkeys/add.go
Normal file
74
cmd/sshkeys/add.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sshkeys
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdSSHKeyAdd represents a sub command of ssh-keys to add an SSH public key
|
||||
var CmdSSHKeyAdd = cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Add an SSH public key",
|
||||
Description: "Add an SSH public key to the current user's profile",
|
||||
ArgsUsage: "<key-file>",
|
||||
Action: RunSSHKeyAdd,
|
||||
Flags: append([]cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "title",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Title for the key (defaults to the filename without extension)",
|
||||
},
|
||||
}, flags.LoginOutputFlags...),
|
||||
}
|
||||
|
||||
// RunSSHKeyAdd reads a public key file and registers it with the Gitea instance
|
||||
func RunSSHKeyAdd(_ stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() < 1 {
|
||||
return fmt.Errorf("key file path is required")
|
||||
}
|
||||
|
||||
keyFile := ctx.Args().First()
|
||||
keyBytes, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read key file '%s': %w", keyFile, err)
|
||||
}
|
||||
|
||||
keyContent := strings.TrimSpace(string(keyBytes))
|
||||
if keyContent == "" {
|
||||
return fmt.Errorf("key file '%s' is empty", keyFile)
|
||||
}
|
||||
|
||||
title := ctx.String("title")
|
||||
if title == "" {
|
||||
base := filepath.Base(keyFile)
|
||||
title = strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
|
||||
key, _, err := ctx.Login.Client().CreatePublicKey(gitea.CreateKeyOption{
|
||||
Title: title,
|
||||
Key: keyContent,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Key '%s' (id: %d) added successfully.\n", key.Title, key.ID)
|
||||
return nil
|
||||
}
|
||||
33
cmd/sshkeys/add_test.go
Normal file
33
cmd/sshkeys/add_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sshkeys
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeyTitleFromFilename(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"id_ed25519.pub", "id_ed25519"},
|
||||
{"id_rsa.pub", "id_rsa"},
|
||||
{"/home/user/.ssh/id_ed25519.pub", "id_ed25519"},
|
||||
{"mykey", "mykey"}, // no extension
|
||||
{"my.key.pub", "my.key"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
base := filepath.Base(tc.input)
|
||||
title := strings.TrimSuffix(base, filepath.Ext(base))
|
||||
assert.Equal(t, tc.expected, title)
|
||||
})
|
||||
}
|
||||
}
|
||||
76
cmd/sshkeys/delete.go
Normal file
76
cmd/sshkeys/delete.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sshkeys
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdSSHKeyDelete represents a sub command of ssh-keys to delete an SSH key by ID
|
||||
var CmdSSHKeyDelete = cli.Command{
|
||||
Name: "delete",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "Delete an SSH key",
|
||||
Description: "Delete an SSH key from the current user's profile by its numeric ID",
|
||||
ArgsUsage: "<key-id>",
|
||||
Action: RunSSHKeyDelete,
|
||||
Flags: append([]cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "confirm",
|
||||
Aliases: []string{"y"},
|
||||
Usage: "Confirm deletion (required)",
|
||||
},
|
||||
}, flags.LoginOutputFlags...),
|
||||
}
|
||||
|
||||
// RunSSHKeyDelete removes an SSH key by its numeric ID
|
||||
func RunSSHKeyDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() < 1 {
|
||||
return fmt.Errorf("key ID is required")
|
||||
}
|
||||
|
||||
keyID, err := strconv.ParseInt(ctx.Args().First(), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid key ID '%s': must be a number", ctx.Args().First())
|
||||
}
|
||||
|
||||
client := ctx.Login.Client()
|
||||
|
||||
key, resp, err := client.GetPublicKey(keyID)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode == 404 {
|
||||
return fmt.Errorf("SSH key with ID %d not found", keyID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if !ctx.Bool("confirm") {
|
||||
fmt.Printf("Are you sure you want to delete SSH key '%s' (id: %d)? [y/N] ", key.Title, keyID)
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if response != "y" && response != "Y" && response != "yes" {
|
||||
fmt.Println("Deletion canceled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = client.DeletePublicKey(keyID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("SSH key '%s' deleted successfully\n", key.Title)
|
||||
return nil
|
||||
}
|
||||
47
cmd/sshkeys/list.go
Normal file
47
cmd/sshkeys/list.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sshkeys
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdSSHKeyList represents a sub command of ssh-keys to list the current user's SSH keys
|
||||
var CmdSSHKeyList = cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "List SSH keys",
|
||||
Description: "List the SSH keys registered for the current user",
|
||||
ArgsUsage: " ", // command does not accept arguments
|
||||
Action: RunSSHKeyList,
|
||||
Flags: append([]cli.Flag{
|
||||
&flags.PaginationPageFlag,
|
||||
&flags.PaginationLimitFlag,
|
||||
}, flags.LoginOutputFlags...),
|
||||
}
|
||||
|
||||
// RunSSHKeyList lists SSH keys for the current user
|
||||
func RunSSHKeyList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := ctx.Login.Client()
|
||||
|
||||
keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||
ListOptions: flags.GetListOptions(cmd),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return print.SSHKeysList(keys, ctx.Output)
|
||||
}
|
||||
44
docs/CLI.md
44
docs/CLI.md
@@ -1941,6 +1941,50 @@ Clone a repository locally
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
## ssh-keys, ssh-key
|
||||
|
||||
Manage SSH public keys
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--page, -p**="": specify page (default: 1)
|
||||
|
||||
### list, ls
|
||||
|
||||
List SSH keys
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--page, -p**="": specify page (default: 1)
|
||||
|
||||
### add
|
||||
|
||||
Add an SSH public key
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--title, -t**="": Title for the key (defaults to the filename without extension)
|
||||
|
||||
### delete, rm
|
||||
|
||||
Delete an SSH key
|
||||
|
||||
**--confirm, -y**: Confirm deletion (required)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
## admin, a
|
||||
|
||||
Operations requiring admin access on the Gitea instance
|
||||
|
||||
44
modules/print/sshkey.go
Normal file
44
modules/print/sshkey.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
// SSHKeysList prints a table of SSH public keys
|
||||
func SSHKeysList(keys []*gitea.PublicKey, output string) error {
|
||||
if len(keys) == 0 {
|
||||
fmt.Printf("No SSH keys found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
t := tableWithHeader(
|
||||
"ID",
|
||||
"Title",
|
||||
"Fingerprint",
|
||||
"KeyType",
|
||||
"ReadOnly",
|
||||
"Created",
|
||||
)
|
||||
|
||||
for _, k := range keys {
|
||||
readOnly := "false"
|
||||
if k.ReadOnly {
|
||||
readOnly = "true"
|
||||
}
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", k.ID),
|
||||
k.Title,
|
||||
k.Fingerprint,
|
||||
k.KeyType,
|
||||
readOnly,
|
||||
FormatTime(k.Created, false),
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
104
tests/integration/helpers_test.go
Normal file
104
tests/integration/helpers_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
integrationGiteaURL string
|
||||
integrationUsername string
|
||||
integrationPassword string
|
||||
integrationToken string
|
||||
integrationTokenID int64
|
||||
integrationSetupErr error
|
||||
integrationClient *gitea.Client
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
integrationGiteaURL = os.Getenv("GITEA_TEA_TEST_URL")
|
||||
integrationUsername = os.Getenv("GITEA_TEA_TEST_USERNAME")
|
||||
integrationPassword = os.Getenv("GITEA_TEA_TEST_PASSWORD")
|
||||
|
||||
if integrationGiteaURL != "" {
|
||||
if integrationUsername == "" || integrationPassword == "" {
|
||||
integrationSetupErr = fmt.Errorf("GITEA_TEA_TEST_USERNAME and GITEA_TEA_TEST_PASSWORD are required for integration tests")
|
||||
} else {
|
||||
integrationClient, integrationSetupErr = gitea.NewClient(
|
||||
integrationGiteaURL,
|
||||
gitea.SetBasicAuth(integrationUsername, integrationPassword),
|
||||
gitea.SetGiteaVersion(""),
|
||||
)
|
||||
if integrationSetupErr == nil {
|
||||
tokenName := fmt.Sprintf("tea-integration-%d", time.Now().UnixNano())
|
||||
var token *gitea.AccessToken
|
||||
token, _, integrationSetupErr = integrationClient.CreateAccessToken(gitea.CreateAccessTokenOption{
|
||||
Name: tokenName,
|
||||
Scopes: []gitea.AccessTokenScope{gitea.AccessTokenScopeAll},
|
||||
})
|
||||
if integrationSetupErr == nil {
|
||||
integrationToken = token.Token
|
||||
integrationTokenID = token.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exitCode := m.Run()
|
||||
|
||||
if integrationClient != nil && integrationTokenID != 0 {
|
||||
if _, err := integrationClient.DeleteAccessToken(integrationTokenID); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to delete integration token %d: %v\n", integrationTokenID, err)
|
||||
if exitCode == 0 {
|
||||
exitCode = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func useTempConfigPath(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "config.yml")
|
||||
config.SetConfigPathForTesting(configPath)
|
||||
config.SetConfigForTesting(config.LocalConfig{})
|
||||
t.Cleanup(func() {
|
||||
config.SetConfigForTesting(config.LocalConfig{})
|
||||
config.SetConfigPathForTesting("")
|
||||
})
|
||||
|
||||
return configPath
|
||||
}
|
||||
|
||||
func createIntegrationLogin(t *testing.T) *config.Login {
|
||||
t.Helper()
|
||||
|
||||
_ = useTempConfigPath(t)
|
||||
if integrationGiteaURL == "" {
|
||||
t.Skip("GITEA_TEA_TEST_URL is not set, skipping integration test")
|
||||
}
|
||||
require.NoError(t, integrationSetupErr)
|
||||
|
||||
require.NotEmpty(t, integrationToken, "integration token setup failed")
|
||||
|
||||
require.NoError(t, task.CreateLogin("integration", integrationToken, "", "", "", "", "", integrationGiteaURL, "", "", true, false, false, false))
|
||||
|
||||
login, err := config.GetLoginByName("integration")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, login)
|
||||
|
||||
return login
|
||||
}
|
||||
@@ -6,58 +6,18 @@ package integration
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/cmd/repos"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func useTempConfigPath(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "config.yml")
|
||||
config.SetConfigPathForTesting(configPath)
|
||||
t.Cleanup(func() {
|
||||
config.SetConfigPathForTesting("")
|
||||
})
|
||||
|
||||
return configPath
|
||||
}
|
||||
|
||||
func createIntegrationLogin(t *testing.T, giteaURL string) *config.Login {
|
||||
t.Helper()
|
||||
|
||||
_ = useTempConfigPath(t)
|
||||
|
||||
username := os.Getenv("GITEA_TEA_TEST_USERNAME")
|
||||
password := os.Getenv("GITEA_TEA_TEST_PASSWORD")
|
||||
require.NotEmpty(t, username, "GITEA_TEA_TEST_USERNAME is required for integration tests")
|
||||
require.NotEmpty(t, password, "GITEA_TEA_TEST_PASSWORD is required for integration tests")
|
||||
|
||||
require.NoError(t, task.CreateLogin("integration", "", username, password, "", "", "", giteaURL, "", "", true, false, false, false))
|
||||
|
||||
login, err := config.GetLoginByName("integration")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, login)
|
||||
|
||||
return login
|
||||
}
|
||||
|
||||
func TestCreateRepoObjectFormat(t *testing.T) {
|
||||
giteaURL := os.Getenv("GITEA_TEA_TEST_URL")
|
||||
if giteaURL == "" {
|
||||
t.Skip("GITEA_TEA_TEST_URL is not set, skipping test")
|
||||
}
|
||||
|
||||
login := createIntegrationLogin(t, giteaURL)
|
||||
login := createIntegrationLogin(t)
|
||||
client := login.Client()
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
|
||||
115
tests/integration/sshkeys_test.go
Normal file
115
tests/integration/sshkeys_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
sshkeyscmd "code.gitea.io/tea/cmd/sshkeys"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// generateTestPublicKey creates a fresh ed25519 keypair and returns a temp
|
||||
// file path containing the public key in authorized_keys format.
|
||||
func generateTestPublicKey(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
sshPub, err := ssh.NewPublicKey(priv.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
pubKeyStr := fmt.Sprintf("ssh-ed25519 %s tea-test-key", base64.StdEncoding.EncodeToString(sshPub.Marshal()))
|
||||
|
||||
f, err := os.CreateTemp(t.TempDir(), "test-*.pub")
|
||||
require.NoError(t, err)
|
||||
_, err = f.WriteString(pubKeyStr)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func sshKeysCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ssh-keys",
|
||||
Commands: []*cli.Command{
|
||||
&sshkeyscmd.CmdSSHKeyList,
|
||||
&sshkeyscmd.CmdSSHKeyAdd,
|
||||
&sshkeyscmd.CmdSSHKeyDelete,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHKeyAddAndDelete(t *testing.T) {
|
||||
login := createIntegrationLogin(t)
|
||||
pubKeyFile := generateTestPublicKey(t)
|
||||
keyTitle := fmt.Sprintf("tea-test-%d", time.Now().Unix())
|
||||
|
||||
cmd := sshKeysCmd()
|
||||
client := login.Client()
|
||||
|
||||
err := cmd.Run(context.Background(), []string{
|
||||
"ssh-keys", "add", pubKeyFile,
|
||||
"--title", keyTitle,
|
||||
"--login", login.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var addedKey *gitea.PublicKey
|
||||
for _, key := range keys {
|
||||
if key.Title == keyTitle {
|
||||
addedKey = key
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, addedKey, "added key not found in key list")
|
||||
|
||||
t.Cleanup(func() {
|
||||
client.DeletePublicKey(addedKey.ID) //nolint:errcheck
|
||||
})
|
||||
|
||||
err = cmd.Run(context.Background(), []string{
|
||||
"ssh-keys", "delete", strconv.FormatInt(addedKey.ID, 10),
|
||||
"--confirm",
|
||||
"--login", login.Name,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, resp, err := client.GetPublicKey(addedKey.ID)
|
||||
assert.Error(t, err)
|
||||
if assert.NotNil(t, resp) {
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHKeyList(t *testing.T) {
|
||||
login := createIntegrationLogin(t)
|
||||
|
||||
cmd := sshKeysCmd()
|
||||
err := cmd.Run(context.Background(), []string{
|
||||
"ssh-keys", "list",
|
||||
"--login", login.Name,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user