mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
fix: support git credential dry-run (#1390)
* fix: support git credential dry-run * test: cover git credential dry-run output
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -61,7 +62,15 @@ var AppsGitCredentialInit = common.Shortcut{
|
||||
return common.NewDryRunAPI().
|
||||
GET(gitCredentialIssuePath).
|
||||
Desc("Issue a Miaoda Git repository PAT").
|
||||
Set("mode", "api-plus-local-setup").
|
||||
Set("action", "initialize_local_git_credential").
|
||||
Set("app_id", appID).
|
||||
Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)).
|
||||
Set("local_effects", []string{
|
||||
"save the issued PAT in the local system credential store",
|
||||
"write app-scoped git credential metadata",
|
||||
"configure a URL-scoped Git credential helper in global git config when possible",
|
||||
}).
|
||||
Params(gitCredentialIssueParams(appID))
|
||||
},
|
||||
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
||||
@@ -124,6 +133,21 @@ var AppsGitCredentialRemove = common.Shortcut{
|
||||
}
|
||||
return validate.ResourceName(strings.TrimSpace(rctx.Str("app-id")), "--app-id")
|
||||
},
|
||||
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
|
||||
appID := strings.TrimSpace(rctx.Str("app-id"))
|
||||
return common.NewDryRunAPI().
|
||||
Desc("Preview local Git credential cleanup (no API call; would clean up local-only state).").
|
||||
Set("mode", "local-cleanup-only").
|
||||
Set("action", "remove_local_git_credential").
|
||||
Set("app_id", appID).
|
||||
Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)).
|
||||
Set("effects", []string{
|
||||
"read app-scoped git credential metadata",
|
||||
"remove the saved PAT from the local system credential store",
|
||||
"remove the app-scoped Git helper from global git config when present",
|
||||
"delete the local metadata record after cleanup succeeds",
|
||||
})
|
||||
},
|
||||
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
||||
appID := strings.TrimSpace(rctx.Str("app-id"))
|
||||
manager := newGitCredentialManager(appID, rctx.Factory.Keychain, nil)
|
||||
@@ -171,6 +195,17 @@ var AppsGitCredentialList = common.Shortcut{
|
||||
Scopes: []string{},
|
||||
AuthTypes: []string{"user"},
|
||||
HasFormat: true,
|
||||
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
|
||||
return common.NewDryRunAPI().
|
||||
Desc("Preview local Git credential listing (no API call, read-only local state).").
|
||||
Set("mode", "local-read-only").
|
||||
Set("action", "list_local_git_credentials").
|
||||
Set("storage_root", filepath.Join(core.GetConfigDir(), storageRoot)).
|
||||
Set("reads", []string{
|
||||
"scan app-scoped git credential metadata under the CLI config directory",
|
||||
"derive per-app repository URLs and local credential status from local metadata",
|
||||
})
|
||||
},
|
||||
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
||||
records, err := listGitCredentialRecords(rctx.Factory.Keychain, time.Now)
|
||||
if err != nil {
|
||||
|
||||
@@ -45,6 +45,11 @@ func TestAppsGitCredentialInitDryRunRequestShape(t *testing.T) {
|
||||
Params map[string]interface{} `json:"params"`
|
||||
Body interface{} `json:"body"`
|
||||
} `json:"api"`
|
||||
Mode string `json:"mode"`
|
||||
Action string `json:"action"`
|
||||
AppID string `json:"app_id"`
|
||||
MetadataFile string `json:"metadata_file"`
|
||||
LocalEffects []string `json:"local_effects"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil {
|
||||
t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String())
|
||||
@@ -65,6 +70,107 @@ func TestAppsGitCredentialInitDryRunRequestShape(t *testing.T) {
|
||||
if call.Body != nil {
|
||||
t.Fatalf("body = %#v, want nil", call.Body)
|
||||
}
|
||||
if payload.Mode != "api-plus-local-setup" {
|
||||
t.Fatalf("mode = %q", payload.Mode)
|
||||
}
|
||||
if payload.Action != "initialize_local_git_credential" {
|
||||
t.Fatalf("action = %q", payload.Action)
|
||||
}
|
||||
if payload.AppID != "app_xxx" {
|
||||
t.Fatalf("app_id = %q", payload.AppID)
|
||||
}
|
||||
if !strings.HasSuffix(payload.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) {
|
||||
t.Fatalf("metadata_file = %q", payload.MetadataFile)
|
||||
}
|
||||
assertStringSliceEqual(t, payload.LocalEffects, []string{
|
||||
"save the issued PAT in the local system credential store",
|
||||
"write app-scoped git credential metadata",
|
||||
"configure a URL-scoped Git credential helper in global git config when possible",
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsGitCredentialListDryRunDescribesLocalReads(t *testing.T) {
|
||||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||||
if err := runAppsShortcut(t, AppsGitCredentialList,
|
||||
[]string{"+git-credential-list", "--dry-run", "--as", "user"},
|
||||
factory, stdout); err != nil {
|
||||
t.Fatalf("dry-run err=%v", err)
|
||||
}
|
||||
var payload struct {
|
||||
Description string `json:"description"`
|
||||
API []interface{} `json:"api"`
|
||||
Mode string `json:"mode"`
|
||||
Action string `json:"action"`
|
||||
StorageRoot string `json:"storage_root"`
|
||||
Reads []string `json:"reads"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil {
|
||||
t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if payload.Description != "Preview local Git credential listing (no API call, read-only local state)." {
|
||||
t.Fatalf("description = %q", payload.Description)
|
||||
}
|
||||
if len(payload.API) != 0 {
|
||||
t.Fatalf("api len = %d, want 0", len(payload.API))
|
||||
}
|
||||
if payload.Mode != "local-read-only" {
|
||||
t.Fatalf("mode = %q", payload.Mode)
|
||||
}
|
||||
if payload.Action != "list_local_git_credentials" {
|
||||
t.Fatalf("action = %q", payload.Action)
|
||||
}
|
||||
if !strings.HasSuffix(payload.StorageRoot, filepath.Join("spark")) {
|
||||
t.Fatalf("storage_root = %q", payload.StorageRoot)
|
||||
}
|
||||
assertStringSliceEqual(t, payload.Reads, []string{
|
||||
"scan app-scoped git credential metadata under the CLI config directory",
|
||||
"derive per-app repository URLs and local credential status from local metadata",
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsGitCredentialRemoveDryRunDescribesLocalCleanup(t *testing.T) {
|
||||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||||
if err := runAppsShortcut(t, AppsGitCredentialRemove,
|
||||
[]string{"+git-credential-remove", "--app-id", "app_xxx", "--dry-run", "--as", "user"},
|
||||
factory, stdout); err != nil {
|
||||
t.Fatalf("dry-run err=%v", err)
|
||||
}
|
||||
var payload struct {
|
||||
Description string `json:"description"`
|
||||
API []interface{} `json:"api"`
|
||||
Mode string `json:"mode"`
|
||||
Action string `json:"action"`
|
||||
AppID string `json:"app_id"`
|
||||
MetadataFile string `json:"metadata_file"`
|
||||
Effects []string `json:"effects"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil {
|
||||
t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if payload.Description != "Preview local Git credential cleanup (no API call; would clean up local-only state)." {
|
||||
t.Fatalf("description = %q", payload.Description)
|
||||
}
|
||||
if len(payload.API) != 0 {
|
||||
t.Fatalf("api len = %d, want 0", len(payload.API))
|
||||
}
|
||||
if payload.Mode != "local-cleanup-only" {
|
||||
t.Fatalf("mode = %q", payload.Mode)
|
||||
}
|
||||
if payload.Action != "remove_local_git_credential" {
|
||||
t.Fatalf("action = %q", payload.Action)
|
||||
}
|
||||
if payload.AppID != "app_xxx" {
|
||||
t.Fatalf("app_id = %q", payload.AppID)
|
||||
}
|
||||
if !strings.HasSuffix(payload.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) {
|
||||
t.Fatalf("metadata_file = %q", payload.MetadataFile)
|
||||
}
|
||||
assertStringSliceEqual(t, payload.Effects, []string{
|
||||
"read app-scoped git credential metadata",
|
||||
"remove the saved PAT from the local system credential store",
|
||||
"remove the app-scoped Git helper from global git config when present",
|
||||
"delete the local metadata record after cleanup succeeds",
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsGitCredentialInitRequiresAppID(t *testing.T) {
|
||||
@@ -579,6 +685,18 @@ func TestAppsGitCredentialRemoveReturnsStoreError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func assertStringSliceEqual(t *testing.T, got, want []string) {
|
||||
t.Helper()
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("slice len = %d, want %d; got %#v", len(got), len(want), got)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("slice[%d] = %q, want %q; got %#v", i, got[i], want[i], got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCredentialLocalErrorWrapsOnlyPlainErrors(t *testing.T) {
|
||||
plain := errors.New("git config failed")
|
||||
wrapped := gitCredentialLocalError("List local Miaoda Git credentials", plain)
|
||||
|
||||
@@ -5,6 +5,8 @@ package apps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -26,7 +28,8 @@ func TestAppsGitCredentialInitDryRun(t *testing.T) {
|
||||
"--app-id", "app_xxx",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
BinaryPath: "../../../lark-cli",
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -35,4 +38,56 @@ func TestAppsGitCredentialInitDryRun(t *testing.T) {
|
||||
assert.Equal(t, "/open-apis/spark/v1/apps/app_xxx/git_info", gjson.Get(result.Stdout, "api.0.url").String())
|
||||
assert.Equal(t, "app_xxx", gjson.Get(result.Stdout, "api.0.params.app_id").String())
|
||||
assert.False(t, gjson.Get(result.Stdout, "api.0.body").Exists())
|
||||
assert.Equal(t, "api-plus-local-setup", gjson.Get(result.Stdout, "mode").String())
|
||||
assert.Equal(t, "initialize_local_git_credential", gjson.Get(result.Stdout, "action").String())
|
||||
assert.True(t, strings.HasSuffix(gjson.Get(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json")))
|
||||
assert.Equal(t, int64(3), gjson.Get(result.Stdout, "local_effects.#").Int())
|
||||
assert.Equal(t, "save the issued PAT in the local system credential store", gjson.Get(result.Stdout, "local_effects.0").String())
|
||||
assert.Equal(t, "write app-scoped git credential metadata", gjson.Get(result.Stdout, "local_effects.1").String())
|
||||
assert.Equal(t, "configure a URL-scoped Git credential helper in global git config when possible", gjson.Get(result.Stdout, "local_effects.2").String())
|
||||
}
|
||||
|
||||
func TestAppsGitCredentialListDryRun(t *testing.T) {
|
||||
setAppsDryRunEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+git-credential-list", "--dry-run"},
|
||||
BinaryPath: "../../../lark-cli",
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
assert.Equal(t, "Preview local Git credential listing (no API call, read-only local state).", gjson.Get(result.Stdout, "description").String())
|
||||
assert.Equal(t, "local-read-only", gjson.Get(result.Stdout, "mode").String())
|
||||
assert.Equal(t, "list_local_git_credentials", gjson.Get(result.Stdout, "action").String())
|
||||
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int())
|
||||
assert.Contains(t, gjson.Get(result.Stdout, "storage_root").String(), filepath.Join("", "spark"))
|
||||
assert.Equal(t, "scan app-scoped git credential metadata under the CLI config directory", gjson.Get(result.Stdout, "reads.0").String())
|
||||
}
|
||||
|
||||
func TestAppsGitCredentialRemoveDryRun(t *testing.T) {
|
||||
setAppsDryRunEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+git-credential-remove", "--app-id", "app_xxx", "--dry-run"},
|
||||
BinaryPath: "../../../lark-cli",
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
assert.Equal(t, "Preview local Git credential cleanup (no API call; would clean up local-only state).", gjson.Get(result.Stdout, "description").String())
|
||||
assert.Equal(t, "local-cleanup-only", gjson.Get(result.Stdout, "mode").String())
|
||||
assert.Equal(t, "remove_local_git_credential", gjson.Get(result.Stdout, "action").String())
|
||||
assert.Equal(t, "app_xxx", gjson.Get(result.Stdout, "app_id").String())
|
||||
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int())
|
||||
assert.True(t, strings.HasSuffix(gjson.Get(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json")))
|
||||
assert.Equal(t, "read app-scoped git credential metadata", gjson.Get(result.Stdout, "effects.0").String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user