Skip to content

Commit 6d8dc40

Browse files
authored
fix: support git credential dry-run (#1390)
* fix: support git credential dry-run * test: cover git credential dry-run output
1 parent 9f2e049 commit 6d8dc40

3 files changed

Lines changed: 209 additions & 1 deletion

File tree

shortcuts/apps/git_credential.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"net/http"
1313
"net/url"
14+
"path/filepath"
1415
"sort"
1516
"strconv"
1617
"strings"
@@ -61,7 +62,15 @@ var AppsGitCredentialInit = common.Shortcut{
6162
return common.NewDryRunAPI().
6263
GET(gitCredentialIssuePath).
6364
Desc("Issue a Miaoda Git repository PAT").
65+
Set("mode", "api-plus-local-setup").
66+
Set("action", "initialize_local_git_credential").
6467
Set("app_id", appID).
68+
Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)).
69+
Set("local_effects", []string{
70+
"save the issued PAT in the local system credential store",
71+
"write app-scoped git credential metadata",
72+
"configure a URL-scoped Git credential helper in global git config when possible",
73+
}).
6574
Params(gitCredentialIssueParams(appID))
6675
},
6776
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
@@ -124,6 +133,21 @@ var AppsGitCredentialRemove = common.Shortcut{
124133
}
125134
return validate.ResourceName(strings.TrimSpace(rctx.Str("app-id")), "--app-id")
126135
},
136+
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
137+
appID := strings.TrimSpace(rctx.Str("app-id"))
138+
return common.NewDryRunAPI().
139+
Desc("Preview local Git credential cleanup (no API call; would clean up local-only state).").
140+
Set("mode", "local-cleanup-only").
141+
Set("action", "remove_local_git_credential").
142+
Set("app_id", appID).
143+
Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)).
144+
Set("effects", []string{
145+
"read app-scoped git credential metadata",
146+
"remove the saved PAT from the local system credential store",
147+
"remove the app-scoped Git helper from global git config when present",
148+
"delete the local metadata record after cleanup succeeds",
149+
})
150+
},
127151
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
128152
appID := strings.TrimSpace(rctx.Str("app-id"))
129153
manager := newGitCredentialManager(appID, rctx.Factory.Keychain, nil)
@@ -171,6 +195,17 @@ var AppsGitCredentialList = common.Shortcut{
171195
Scopes: []string{},
172196
AuthTypes: []string{"user"},
173197
HasFormat: true,
198+
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
199+
return common.NewDryRunAPI().
200+
Desc("Preview local Git credential listing (no API call, read-only local state).").
201+
Set("mode", "local-read-only").
202+
Set("action", "list_local_git_credentials").
203+
Set("storage_root", filepath.Join(core.GetConfigDir(), storageRoot)).
204+
Set("reads", []string{
205+
"scan app-scoped git credential metadata under the CLI config directory",
206+
"derive per-app repository URLs and local credential status from local metadata",
207+
})
208+
},
174209
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
175210
records, err := listGitCredentialRecords(rctx.Factory.Keychain, time.Now)
176211
if err != nil {

shortcuts/apps/git_credential_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ func TestAppsGitCredentialInitDryRunRequestShape(t *testing.T) {
4545
Params map[string]interface{} `json:"params"`
4646
Body interface{} `json:"body"`
4747
} `json:"api"`
48+
Mode string `json:"mode"`
49+
Action string `json:"action"`
50+
AppID string `json:"app_id"`
51+
MetadataFile string `json:"metadata_file"`
52+
LocalEffects []string `json:"local_effects"`
4853
}
4954
if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil {
5055
t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String())
@@ -65,6 +70,107 @@ func TestAppsGitCredentialInitDryRunRequestShape(t *testing.T) {
6570
if call.Body != nil {
6671
t.Fatalf("body = %#v, want nil", call.Body)
6772
}
73+
if payload.Mode != "api-plus-local-setup" {
74+
t.Fatalf("mode = %q", payload.Mode)
75+
}
76+
if payload.Action != "initialize_local_git_credential" {
77+
t.Fatalf("action = %q", payload.Action)
78+
}
79+
if payload.AppID != "app_xxx" {
80+
t.Fatalf("app_id = %q", payload.AppID)
81+
}
82+
if !strings.HasSuffix(payload.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) {
83+
t.Fatalf("metadata_file = %q", payload.MetadataFile)
84+
}
85+
assertStringSliceEqual(t, payload.LocalEffects, []string{
86+
"save the issued PAT in the local system credential store",
87+
"write app-scoped git credential metadata",
88+
"configure a URL-scoped Git credential helper in global git config when possible",
89+
})
90+
}
91+
92+
func TestAppsGitCredentialListDryRunDescribesLocalReads(t *testing.T) {
93+
factory, stdout, _ := newAppsExecuteFactory(t)
94+
if err := runAppsShortcut(t, AppsGitCredentialList,
95+
[]string{"+git-credential-list", "--dry-run", "--as", "user"},
96+
factory, stdout); err != nil {
97+
t.Fatalf("dry-run err=%v", err)
98+
}
99+
var payload struct {
100+
Description string `json:"description"`
101+
API []interface{} `json:"api"`
102+
Mode string `json:"mode"`
103+
Action string `json:"action"`
104+
StorageRoot string `json:"storage_root"`
105+
Reads []string `json:"reads"`
106+
}
107+
if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil {
108+
t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String())
109+
}
110+
if payload.Description != "Preview local Git credential listing (no API call, read-only local state)." {
111+
t.Fatalf("description = %q", payload.Description)
112+
}
113+
if len(payload.API) != 0 {
114+
t.Fatalf("api len = %d, want 0", len(payload.API))
115+
}
116+
if payload.Mode != "local-read-only" {
117+
t.Fatalf("mode = %q", payload.Mode)
118+
}
119+
if payload.Action != "list_local_git_credentials" {
120+
t.Fatalf("action = %q", payload.Action)
121+
}
122+
if !strings.HasSuffix(payload.StorageRoot, filepath.Join("spark")) {
123+
t.Fatalf("storage_root = %q", payload.StorageRoot)
124+
}
125+
assertStringSliceEqual(t, payload.Reads, []string{
126+
"scan app-scoped git credential metadata under the CLI config directory",
127+
"derive per-app repository URLs and local credential status from local metadata",
128+
})
129+
}
130+
131+
func TestAppsGitCredentialRemoveDryRunDescribesLocalCleanup(t *testing.T) {
132+
factory, stdout, _ := newAppsExecuteFactory(t)
133+
if err := runAppsShortcut(t, AppsGitCredentialRemove,
134+
[]string{"+git-credential-remove", "--app-id", "app_xxx", "--dry-run", "--as", "user"},
135+
factory, stdout); err != nil {
136+
t.Fatalf("dry-run err=%v", err)
137+
}
138+
var payload struct {
139+
Description string `json:"description"`
140+
API []interface{} `json:"api"`
141+
Mode string `json:"mode"`
142+
Action string `json:"action"`
143+
AppID string `json:"app_id"`
144+
MetadataFile string `json:"metadata_file"`
145+
Effects []string `json:"effects"`
146+
}
147+
if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil {
148+
t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String())
149+
}
150+
if payload.Description != "Preview local Git credential cleanup (no API call; would clean up local-only state)." {
151+
t.Fatalf("description = %q", payload.Description)
152+
}
153+
if len(payload.API) != 0 {
154+
t.Fatalf("api len = %d, want 0", len(payload.API))
155+
}
156+
if payload.Mode != "local-cleanup-only" {
157+
t.Fatalf("mode = %q", payload.Mode)
158+
}
159+
if payload.Action != "remove_local_git_credential" {
160+
t.Fatalf("action = %q", payload.Action)
161+
}
162+
if payload.AppID != "app_xxx" {
163+
t.Fatalf("app_id = %q", payload.AppID)
164+
}
165+
if !strings.HasSuffix(payload.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) {
166+
t.Fatalf("metadata_file = %q", payload.MetadataFile)
167+
}
168+
assertStringSliceEqual(t, payload.Effects, []string{
169+
"read app-scoped git credential metadata",
170+
"remove the saved PAT from the local system credential store",
171+
"remove the app-scoped Git helper from global git config when present",
172+
"delete the local metadata record after cleanup succeeds",
173+
})
68174
}
69175

70176
func TestAppsGitCredentialInitRequiresAppID(t *testing.T) {
@@ -579,6 +685,18 @@ func TestAppsGitCredentialRemoveReturnsStoreError(t *testing.T) {
579685
}
580686
}
581687

688+
func assertStringSliceEqual(t *testing.T, got, want []string) {
689+
t.Helper()
690+
if len(got) != len(want) {
691+
t.Fatalf("slice len = %d, want %d; got %#v", len(got), len(want), got)
692+
}
693+
for i := range want {
694+
if got[i] != want[i] {
695+
t.Fatalf("slice[%d] = %q, want %q; got %#v", i, got[i], want[i], got)
696+
}
697+
}
698+
}
699+
582700
func TestGitCredentialLocalErrorWrapsOnlyPlainErrors(t *testing.T) {
583701
plain := errors.New("git config failed")
584702
wrapped := gitCredentialLocalError("List local Miaoda Git credentials", plain)

tests/cli_e2e/apps/apps_git_credential_dryrun_test.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package apps
55

66
import (
77
"context"
8+
"path/filepath"
9+
"strings"
810
"testing"
911
"time"
1012

@@ -26,7 +28,8 @@ func TestAppsGitCredentialInitDryRun(t *testing.T) {
2628
"--app-id", "app_xxx",
2729
"--dry-run",
2830
},
29-
DefaultAs: "user",
31+
BinaryPath: "../../../lark-cli",
32+
DefaultAs: "user",
3033
})
3134
require.NoError(t, err)
3235
result.AssertExitCode(t, 0)
@@ -35,4 +38,56 @@ func TestAppsGitCredentialInitDryRun(t *testing.T) {
3538
assert.Equal(t, "/open-apis/spark/v1/apps/app_xxx/git_info", gjson.Get(result.Stdout, "api.0.url").String())
3639
assert.Equal(t, "app_xxx", gjson.Get(result.Stdout, "api.0.params.app_id").String())
3740
assert.False(t, gjson.Get(result.Stdout, "api.0.body").Exists())
41+
assert.Equal(t, "api-plus-local-setup", gjson.Get(result.Stdout, "mode").String())
42+
assert.Equal(t, "initialize_local_git_credential", gjson.Get(result.Stdout, "action").String())
43+
assert.True(t, strings.HasSuffix(gjson.Get(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json")))
44+
assert.Equal(t, int64(3), gjson.Get(result.Stdout, "local_effects.#").Int())
45+
assert.Equal(t, "save the issued PAT in the local system credential store", gjson.Get(result.Stdout, "local_effects.0").String())
46+
assert.Equal(t, "write app-scoped git credential metadata", gjson.Get(result.Stdout, "local_effects.1").String())
47+
assert.Equal(t, "configure a URL-scoped Git credential helper in global git config when possible", gjson.Get(result.Stdout, "local_effects.2").String())
48+
}
49+
50+
func TestAppsGitCredentialListDryRun(t *testing.T) {
51+
setAppsDryRunEnv(t)
52+
53+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
54+
t.Cleanup(cancel)
55+
56+
result, err := clie2e.RunCmd(ctx, clie2e.Request{
57+
Args: []string{"apps", "+git-credential-list", "--dry-run"},
58+
BinaryPath: "../../../lark-cli",
59+
DefaultAs: "user",
60+
})
61+
require.NoError(t, err)
62+
result.AssertExitCode(t, 0)
63+
64+
assert.Equal(t, "Preview local Git credential listing (no API call, read-only local state).", gjson.Get(result.Stdout, "description").String())
65+
assert.Equal(t, "local-read-only", gjson.Get(result.Stdout, "mode").String())
66+
assert.Equal(t, "list_local_git_credentials", gjson.Get(result.Stdout, "action").String())
67+
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int())
68+
assert.Contains(t, gjson.Get(result.Stdout, "storage_root").String(), filepath.Join("", "spark"))
69+
assert.Equal(t, "scan app-scoped git credential metadata under the CLI config directory", gjson.Get(result.Stdout, "reads.0").String())
70+
}
71+
72+
func TestAppsGitCredentialRemoveDryRun(t *testing.T) {
73+
setAppsDryRunEnv(t)
74+
75+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
76+
t.Cleanup(cancel)
77+
78+
result, err := clie2e.RunCmd(ctx, clie2e.Request{
79+
Args: []string{"apps", "+git-credential-remove", "--app-id", "app_xxx", "--dry-run"},
80+
BinaryPath: "../../../lark-cli",
81+
DefaultAs: "user",
82+
})
83+
require.NoError(t, err)
84+
result.AssertExitCode(t, 0)
85+
86+
assert.Equal(t, "Preview local Git credential cleanup (no API call; would clean up local-only state).", gjson.Get(result.Stdout, "description").String())
87+
assert.Equal(t, "local-cleanup-only", gjson.Get(result.Stdout, "mode").String())
88+
assert.Equal(t, "remove_local_git_credential", gjson.Get(result.Stdout, "action").String())
89+
assert.Equal(t, "app_xxx", gjson.Get(result.Stdout, "app_id").String())
90+
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int())
91+
assert.True(t, strings.HasSuffix(gjson.Get(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json")))
92+
assert.Equal(t, "read app-scoped git credential metadata", gjson.Get(result.Stdout, "effects.0").String())
3893
}

0 commit comments

Comments
 (0)