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:
linchao5102
2026-06-11 01:49:06 +08:00
committed by GitHub
parent 9f2e049858
commit 6d8dc402ac
3 changed files with 209 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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())
}