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,
|
&CmdNotifications,
|
||||||
&CmdRepoClone,
|
&CmdRepoClone,
|
||||||
|
|
||||||
|
&CmdSSHKeys,
|
||||||
|
|
||||||
&CmdAdmin,
|
&CmdAdmin,
|
||||||
|
|
||||||
&CmdApi,
|
&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
|
**--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
|
## admin, a
|
||||||
|
|
||||||
Operations requiring admin access on the Gitea instance
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"code.gitea.io/tea/cmd/repos"
|
"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/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v3"
|
"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) {
|
func TestCreateRepoObjectFormat(t *testing.T) {
|
||||||
giteaURL := os.Getenv("GITEA_TEA_TEST_URL")
|
login := createIntegrationLogin(t)
|
||||||
if giteaURL == "" {
|
|
||||||
t.Skip("GITEA_TEA_TEST_URL is not set, skipping test")
|
|
||||||
}
|
|
||||||
|
|
||||||
login := createIntegrationLogin(t, giteaURL)
|
|
||||||
client := login.Client()
|
client := login.Client()
|
||||||
timestamp := time.Now().Unix()
|
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