Skip to content

fix: harden auth check and config removal#125

Open
zfaustk wants to merge 10 commits intolarksuite:mainfrom
zfaustk:fix/auth-check-and-config-remove
Open

fix: harden auth check and config removal#125
zfaustk wants to merge 10 commits intolarksuite:mainfrom
zfaustk:fix/auth-check-and-config-remove

Conversation

@zfaustk
Copy link
Copy Markdown

@zfaustk zfaustk commented Mar 31, 2026

Summary

  • reject empty resolved scope lists in auth check
  • save the empty config before clearing secrets/tokens in config remove
  • add focused regression tests for both failure-prone paths

Fixes #116
Fixes #114

Why

auth check --scope "" currently returns {"ok":true} even though nothing was actually checked. That can silently greenlight automation when a dynamically built scope string collapses to empty.

config remove currently clears keychain/token state before persisting the empty config. If the config write fails, the CLI is left in an inconsistent state: config.json still points at the old app/user records, but the secret/token material is already gone.

Testing

  • added TestAuthCheckRun_EmptyScopeReturnsValidationError
  • added TestConfigRemoveRun_SaveFailureDoesNotClearSecretsOrTokens
  • added TestConfigRemoveRun_ClearsSecretsAndTokensAfterSuccessfulSave
  • attempted CGO_ENABLED=0 /ROOM/.toolchains/go1.24.13/bin/go test ./cmd/auth ./cmd/config in this container, but the local Go build/test process did not complete within the timeout window, so I am not claiming a full local go test pass from this environment

Summary by CodeRabbit

  • Bug Fixes

    • Auth check command now properly validates and rejects empty scopes.
    • Config removal command now displays warnings when token cleanup fails instead of silently ignoring errors.
    • Improved input validation for config removal operations.
  • Tests

    • Added test coverage for empty scope validation.
    • Added test coverage for config removal functionality.

Fixes larksuite#116 by rejecting empty --scope values in auth check instead of returning a misleading ok response.

Fixes larksuite#114 by saving an empty config before best-effort secret/token cleanup, and adds focused regression tests for both flows.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 31, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Collaborator

@HanShaoshuai-k HanShaoshuai-k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestions

1. Package-level mutable vars don't follow project conventions (remove.go:16-21) — Blocking

The project consistently uses Factory / TestFactory for dependency injection — HTTP via httpmock.Registry, keychain via noopKeychain, IO via buffer replacement. No other test uses package-level var swapping.

Suggest moving these deps into ConfigRemoveOptions or Factory to stay consistent. The current approach also risks data races if t.Parallel() is ever added.

2. Token cleanup errors silently discarded (remove.go:56) — Medium

_ = removeStoredToken(app.AppId, user.UserOpenId)

If save succeeds but token cleanup fails, orphaned keychain entries remain with no CLI path to clean them. Suggest logging a warning on failure.

3. CLA not signed — Blocking

Please sign the Contributor License Agreement before we can merge.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR addresses two validation and order-of-operations bugs: auth check --scope "" now rejects empty scope with a validation error instead of returning misleading success; config remove now saves the empty config before clearing keychain entries, ensuring consistent state on save failures, and token removal errors now emit warnings instead of failing silently.

Changes

Cohort / File(s) Summary
Auth Check Validation
cmd/auth/check.go, cmd/auth/check_test.go
Modified authCheckRun to reject empty scope with validation error instead of returning success. Added test validating error is returned for empty and whitespace-only scope values.
Config Remove with Dependency Injection
cmd/config/remove.go (inferred), cmd/config/remove_test.go
Added dependency-injection function fields to ConfigRemoveOptions and initialization checks in configRemoveRun. Reordered operations to save config before clearing keychain. Changed token removal failures from silent to warning-level output. Added three comprehensive tests covering injection semantics, error handling, and initialization validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • sang-neo03
  • liangshuo-1

Poem

🐰 Scope cannot be empty—we now loudly protest!
Config saves before secrets go, keeping state blessed.
With warnings when cleanup goes wrong,
No more silent failures along.
Each test proves the logic is strong! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: harden auth check and config removal' directly addresses the main changes: validation hardening for auth check and config removal robustness.
Description check ✅ Passed The description covers Summary, Changes (implicit in the summary bullets), Testing with specific test names, and Related Issues with Fixes links.
Linked Issues check ✅ Passed The PR fully implements both issue objectives: empty scope validation in auth check and config-save-first ordering in config remove with dependency injection and warning-level error handling.
Out of Scope Changes check ✅ Passed All changes are directly related to the two linked issues: empty scope validation, config-save-first reordering, dependency injection for testability, and error handling improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
cmd/auth/check_test.go (1)

16-19: Consider adding a test case for literal empty string.

The current test uses " " (whitespace-only). While strings.Fields handles both identically, testing "" as well would document both edge cases explicitly and guard against future refactoring that might change the parsing logic.

🧪 Optional: Add table-driven test for both edge cases
 func TestAuthCheckRun_EmptyScopeReturnsValidationError(t *testing.T) {
-	f, stdout, _, _ := cmdutil.TestFactory(t, nil)
-
-	err := authCheckRun(&CheckOptions{
-		Factory: f,
-		Scope:   "   ",
-	})
-	if err == nil {
-		t.Fatal("expected validation error")
-	}
-	if !strings.Contains(err.Error(), "--scope cannot be empty") {
-		t.Fatalf("unexpected error: %v", err)
-	}
-	if stdout.Len() != 0 {
-		t.Fatalf("expected no stdout, got %q", stdout.String())
+	cases := []struct {
+		name  string
+		scope string
+	}{
+		{"empty string", ""},
+		{"whitespace only", "   "},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			f, stdout, _, _ := cmdutil.TestFactory(t, nil)
+			err := authCheckRun(&CheckOptions{
+				Factory: f,
+				Scope:   tc.scope,
+			})
+			if err == nil {
+				t.Fatal("expected validation error")
+			}
+			if !strings.Contains(err.Error(), "--scope cannot be empty") {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if stdout.Len() != 0 {
+				t.Fatalf("expected no stdout, got %q", stdout.String())
+			}
+		})
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/auth/check_test.go` around lines 16 - 19, Add an explicit test for the
literal empty string alongside the whitespace-only test: call
authCheckRun(&CheckOptions{Factory: f, Scope: ""}) and assert the same behavior
as the existing `"   "` case; you can also convert the test into a small
table-driven loop over inputs `""` and `"   "` to avoid duplication. Target the
existing test that invokes authCheckRun and uses CheckOptions.Scope so the new
case is exercised by the same assertions.
cmd/config/remove_test.go (1)

83-122: Consider making the save-before-cleanup assertion order-explicit.

Current checks prove save and cleanup both happened, but not strict sequencing. Recording call order would make the regression test stricter.

Example tightening
 	savedEmpty := false
+	callOrder := []string{}
 	saveConfig := func(next *core.MultiAppConfig) error {
+		callOrder = append(callOrder, "save")
 		if len(next.Apps) != 0 {
 			t.Fatalf("expected empty config, got %+v", next.Apps)
 		}
 		savedEmpty = true
 		return nil
 	}
@@
 	removeSecret := func(input core.SecretInput, kc keychain.KeychainAccess) {
+		callOrder = append(callOrder, "remove_secret")
 		if input.Ref != nil {
 			secretRemovals = append(secretRemovals, input.Ref.ID)
 		}
 	}
@@
 	removeStoredToken := func(appID, userOpenID string) error {
+		callOrder = append(callOrder, "remove_token")
 		tokenRemovals = append(tokenRemovals, appID+":"+userOpenID)
 		return nil
 	}
@@
 	if !savedEmpty {
 		t.Fatal("expected empty config to be saved before cleanup")
 	}
+	if len(callOrder) == 0 || callOrder[0] != "save" {
+		t.Fatalf("expected save to happen first, order=%v", callOrder)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/config/remove_test.go` around lines 83 - 122, The test currently verifies
that saveConfig ran and that removeSecret/removeStoredToken performed removals
but doesn't assert the sequence; modify the test to record call order (e.g.,
append markers to a slice) inside saveConfig, removeSecret, and
removeStoredToken and then after configRemoveRun assert the order contains the
save marker before any cleanup markers; update the closures for saveConfig,
removeSecret, and removeStoredToken to push distinct tokens (like "save",
"secret:<id>", "token:<app:user>") into a shared slice and assert that the first
occurrence of "save" precedes the first cleanup token to guarantee
save-before-cleanup ordering for ConfigRemoveOptions/configRemoveRun.
cmd/config/remove.go (1)

50-53: Add defensive guards for injected callbacks to avoid nil-function panics.

With the new injection pattern, a partially initialized ConfigRemoveOptions can panic at runtime. A fast-fail internal error is safer.

Suggested hardening
 func configRemoveRun(opts *ConfigRemoveOptions) error {
+	if opts == nil || opts.Factory == nil ||
+		opts.LoadConfig == nil || opts.SaveConfig == nil ||
+		opts.RemoveSecret == nil || opts.RemoveStoredToken == nil {
+		return output.Errorf(output.ExitInternal, "internal", "config remove options not initialized")
+	}
 	f := opts.Factory
 
 	config, err := opts.LoadConfig()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/config/remove.go` around lines 50 - 53, The function configRemoveRun
should defensively validate the incoming opts and any injected function fields
on ConfigRemoveOptions before calling them: first check opts != nil, then ensure
opts.LoadConfig and any other function-typed callback fields (the injected
callbacks on ConfigRemoveOptions, e.g., any
PreRemoveHook/PostRemoveHook/Validate callbacks present in the struct) are
non-nil and return a clear internal error if any are nil; perform these guards
at the top of configRemoveRun so callers fail fast instead of panicking when
invoking opts.LoadConfig or other callbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/auth/check_test.go`:
- Around line 16-19: Add an explicit test for the literal empty string alongside
the whitespace-only test: call authCheckRun(&CheckOptions{Factory: f, Scope:
""}) and assert the same behavior as the existing `"   "` case; you can also
convert the test into a small table-driven loop over inputs `""` and `"   "` to
avoid duplication. Target the existing test that invokes authCheckRun and uses
CheckOptions.Scope so the new case is exercised by the same assertions.

In `@cmd/config/remove_test.go`:
- Around line 83-122: The test currently verifies that saveConfig ran and that
removeSecret/removeStoredToken performed removals but doesn't assert the
sequence; modify the test to record call order (e.g., append markers to a slice)
inside saveConfig, removeSecret, and removeStoredToken and then after
configRemoveRun assert the order contains the save marker before any cleanup
markers; update the closures for saveConfig, removeSecret, and removeStoredToken
to push distinct tokens (like "save", "secret:<id>", "token:<app:user>") into a
shared slice and assert that the first occurrence of "save" precedes the first
cleanup token to guarantee save-before-cleanup ordering for
ConfigRemoveOptions/configRemoveRun.

In `@cmd/config/remove.go`:
- Around line 50-53: The function configRemoveRun should defensively validate
the incoming opts and any injected function fields on ConfigRemoveOptions before
calling them: first check opts != nil, then ensure opts.LoadConfig and any other
function-typed callback fields (the injected callbacks on ConfigRemoveOptions,
e.g., any PreRemoveHook/PostRemoveHook/Validate callbacks present in the struct)
are non-nil and return a clear internal error if any are nil; perform these
guards at the top of configRemoveRun so callers fail fast instead of panicking
when invoking opts.LoadConfig or other callbacks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e843c71d-7dd9-48e8-81e8-2c9dee89fe34

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd5049 and 13cd61e.

📒 Files selected for processing (4)
  • cmd/auth/check.go
  • cmd/auth/check_test.go
  • cmd/config/remove.go
  • cmd/config/remove_test.go

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR fixes two real bugs — the silent auth check pass-through on empty scopes and the half-removed state left by a failed config remove write — and the underlying logic changes are correct. However, the PR cannot land as-is because cmd/config/remove.go was accidentally renamed to the literal filename undefined at the repository root, and the companion test file was dropped at root rather than cmd/config/, both of which break the build.

  • P0: cmd/config/remove.goundefined at repo root breaks cmd/config (missing NewCmdConfigRemove) and creates an illegal dual-package root (package main + package config).
  • P0: remove_test.go at root declares package config alongside main.go's package main — same illegal dual-package error.
  • P1: The save-failure regression test (TestConfigRemoveRun_SaveFailureDoesNotClearSecretsOrTokens) cited in the PR description is not present; the happy-path ordering test does not exercise the "SaveConfig errors → no cleanup" invariant.

Confidence Score: 2/5

Not safe to merge — two P0 build-breaking file placement errors must be resolved first.

The intended logic is sound, but the accidental rename of cmd/config/remove.go to a file literally named "undefined" at the repo root, plus remove_test.go landing at root instead of cmd/config/, means the entire config package fails to compile. These are hard blockers before any of the correct bug-fix logic can ship.

undefined (must be restored to cmd/config/remove.go) and remove_test.go (must be moved to cmd/config/remove_test.go)

Important Files Changed

Filename Overview
cmd/auth/check.go Replaces the silent no-op empty-scope path with a validation error; correct and minimal change.
cmd/auth/check_test.go New test file covering both empty-string and whitespace-only scope inputs; well-structured and in the correct package directory.
undefined Was cmd/config/remove.go; accidentally renamed to the literal filename "undefined" at the repo root, breaking the cmd/config package build and conflicting with main.go's package main declaration.
remove_test.go Test file placed at repo root with package config, conflicting with main.go; must move to cmd/config/remove_test.go. Also missing the save-failure regression test named in the PR description.

Sequence Diagram

sequenceDiagram
    participant CLI as config remove (CLI)
    participant SaveConfig
    participant Keychain as RemoveSecret / RemoveStoredToken

    CLI->>SaveConfig: SaveConfig(empty config)
    alt SaveConfig fails
        SaveConfig-->>CLI: error
        CLI-->>CLI: return error (secrets untouched)
    else SaveConfig succeeds
        SaveConfig-->>CLI: nil
        loop for each app
            CLI->>Keychain: RemoveSecret(app.AppSecret)
            loop for each user
                CLI->>Keychain: RemoveStoredToken(appId, userId)
                alt token removal fails
                    Keychain-->>CLI: error
                    CLI-->>CLI: warn to stderr (continue)
                end
            end
        end
        CLI-->>CLI: print "Configuration removed"
    end
Loading

Comments Outside Diff (1)

  1. undefined, line 1-5 (link)

    P0 File renamed to literal path undefined — build broken

    cmd/config/remove.go has been renamed to a file literally called undefined at the repository root. This causes two distinct build failures:

    1. cmd/config/config.go calls NewCmdConfigRemove(f, nil) which is no longer in the cmd/config package — the entire config command fails to compile with "undefined: NewCmdConfigRemove".
    2. Go forbids two different package names in the same directory: main.go declares package main while this file declares package config, producing "found packages main and config in /".

    The file needs to be moved back to cmd/config/remove.go for the package to compile again.

Reviews (5): Last reviewed commit: "test(config): avoid asserting leaked use..." | Re-trigger Greptile

@zfaustk
Copy link
Copy Markdown
Author

zfaustk commented Apr 1, 2026

Addressed the requested changes in the latest commit (13cd61e):

  • moved the injected deps back into ConfigRemoveOptions instead of using package-level mutable vars
  • surface token cleanup failures as warnings instead of discarding them
  • CLA is now signed

Could you please take another look when you have time?

@zfaustk zfaustk closed this Apr 4, 2026
@zfaustk zfaustk reopened this Apr 4, 2026
@github-actions github-actions bot added the size/L Large or sensitive change across domains or core paths label Apr 4, 2026
@zfaustk zfaustk requested a review from HanShaoshuai-k April 4, 2026 05:49
@sang-neo03
Copy link
Copy Markdown
Collaborator

@zfaustk please resolve the conflict to merge

@zfaustk
Copy link
Copy Markdown
Author

zfaustk commented Apr 11, 2026

Resolved the merge conflict and refreshed the branch on the latest head (7323a70).

This keeps the scoped follow-up only: empty/whitespace --scope validation, injected callbacks for config remove, token cleanup warnings, and focused tests. Please re-review when you have a chance.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/config/remove_test.go`:
- Around line 16-80: Add a new test that covers the failure path of SaveConfig
to ensure RemoveSecret and RemoveStoredToken are not invoked when saving fails:
duplicate the pattern from
TestConfigRemoveRun_UsesInjectedCallbacksAndSavesBeforeCleanup but make
SaveConfig return a non-nil error (and record callOrder), provide stub
RemoveSecret and RemoveStoredToken that append to slices or callOrder if
invoked, call configRemoveRun with these injected callbacks, assert
configRemoveRun returns the SaveConfig error, verify callOrder contains "save"
as first entry and that no "secret" or "token" entries were appended, and check
stderr contains an appropriate failure message; reference the existing test name
and the configRemoveRun, SaveConfig, RemoveSecret, RemoveStoredToken symbols to
place this new test alongside the current one.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 749106fb-076b-48e7-8b30-939970f65973

📥 Commits

Reviewing files that changed from the base of the PR and between 13cd61e and 7323a70.

📒 Files selected for processing (3)
  • cmd/auth/check_test.go
  • cmd/config/remove.go
  • cmd/config/remove_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/auth/check_test.go
  • cmd/config/remove.go

Comment on lines +16 to +80
func TestConfigRemoveRun_UsesInjectedCallbacksAndSavesBeforeCleanup(t *testing.T) {
f, _, stderr, _ := cmdutil.TestFactory(t, nil)

cfg := &core.MultiAppConfig{
Apps: []core.AppConfig{
{
AppId: "cli_app_a",
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app_a"}},
Users: []core.AppUser{{UserOpenId: "ou_a"}},
},
{
AppId: "cli_app_b",
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app_b"}},
Users: []core.AppUser{{UserOpenId: "ou_b1"}, {UserOpenId: "ou_b2"}},
},
},
}

callOrder := []string{}
saveConfig := func(next *core.MultiAppConfig) error {
callOrder = append(callOrder, "save")
if len(next.Apps) != 0 {
t.Fatalf("expected empty config, got %+v", next.Apps)
}
return nil
}

var secretRemovals []string
removeSecret := func(input core.SecretInput, kc keychain.KeychainAccess) {
callOrder = append(callOrder, "secret")
if input.Ref != nil {
secretRemovals = append(secretRemovals, input.Ref.ID)
}
}

var tokenRemovals []string
removeStoredToken := func(appID, userOpenID string) error {
callOrder = append(callOrder, "token")
tokenRemovals = append(tokenRemovals, appID+":"+userOpenID)
return nil
}

if err := configRemoveRun(&ConfigRemoveOptions{
Factory: f,
LoadConfig: func() (*core.MultiAppConfig, error) { return cfg, nil },
SaveConfig: saveConfig,
RemoveSecret: removeSecret,
RemoveStoredToken: removeStoredToken,
}); err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(callOrder) == 0 || callOrder[0] != "save" {
t.Fatalf("expected save to happen first, order=%v", callOrder)
}
if len(secretRemovals) != 2 {
t.Fatalf("secret removals = %v, want 2 entries", secretRemovals)
}
if len(tokenRemovals) != 3 {
t.Fatalf("token removals = %v, want 3 entries", tokenRemovals)
}
if got := stderr.String(); !strings.Contains(got, "Configuration removed") {
t.Fatalf("expected success message on stderr, got %q", got)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing regression coverage for failed SaveConfig cleanup guard

The success-path ordering check is good, but this suite still doesn’t assert the critical failure invariant: if SaveConfig fails, RemoveSecret and RemoveStoredToken must not run. That behavior is central to issue #114 and should be covered explicitly.

Suggested test to add
+func TestConfigRemoveRun_SaveFailureDoesNotClearSecretsOrTokens(t *testing.T) {
+	f, _, _, _ := cmdutil.TestFactory(t, nil)
+
+	cleanupSecretCalls := 0
+	cleanupTokenCalls := 0
+
+	err := configRemoveRun(&ConfigRemoveOptions{
+		Factory: f,
+		LoadConfig: func() (*core.MultiAppConfig, error) {
+			return &core.MultiAppConfig{
+				Apps: []core.AppConfig{{
+					AppId:     "cli_app",
+					AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app"}},
+					Users:     []core.AppUser{{UserOpenId: "ou_1"}},
+				}},
+			}, nil
+		},
+		SaveConfig: func(*core.MultiAppConfig) error { return errors.New("disk write failed") },
+		RemoveSecret: func(core.SecretInput, keychain.KeychainAccess) {
+			cleanupSecretCalls++
+		},
+		RemoveStoredToken: func(string, string) error {
+			cleanupTokenCalls++
+			return nil
+		},
+	})
+
+	if err == nil {
+		t.Fatal("expected save failure")
+	}
+	if cleanupSecretCalls != 0 || cleanupTokenCalls != 0 {
+		t.Fatalf("cleanup must not run when save fails; secret=%d token=%d", cleanupSecretCalls, cleanupTokenCalls)
+	}
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestConfigRemoveRun_UsesInjectedCallbacksAndSavesBeforeCleanup(t *testing.T) {
f, _, stderr, _ := cmdutil.TestFactory(t, nil)
cfg := &core.MultiAppConfig{
Apps: []core.AppConfig{
{
AppId: "cli_app_a",
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app_a"}},
Users: []core.AppUser{{UserOpenId: "ou_a"}},
},
{
AppId: "cli_app_b",
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app_b"}},
Users: []core.AppUser{{UserOpenId: "ou_b1"}, {UserOpenId: "ou_b2"}},
},
},
}
callOrder := []string{}
saveConfig := func(next *core.MultiAppConfig) error {
callOrder = append(callOrder, "save")
if len(next.Apps) != 0 {
t.Fatalf("expected empty config, got %+v", next.Apps)
}
return nil
}
var secretRemovals []string
removeSecret := func(input core.SecretInput, kc keychain.KeychainAccess) {
callOrder = append(callOrder, "secret")
if input.Ref != nil {
secretRemovals = append(secretRemovals, input.Ref.ID)
}
}
var tokenRemovals []string
removeStoredToken := func(appID, userOpenID string) error {
callOrder = append(callOrder, "token")
tokenRemovals = append(tokenRemovals, appID+":"+userOpenID)
return nil
}
if err := configRemoveRun(&ConfigRemoveOptions{
Factory: f,
LoadConfig: func() (*core.MultiAppConfig, error) { return cfg, nil },
SaveConfig: saveConfig,
RemoveSecret: removeSecret,
RemoveStoredToken: removeStoredToken,
}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(callOrder) == 0 || callOrder[0] != "save" {
t.Fatalf("expected save to happen first, order=%v", callOrder)
}
if len(secretRemovals) != 2 {
t.Fatalf("secret removals = %v, want 2 entries", secretRemovals)
}
if len(tokenRemovals) != 3 {
t.Fatalf("token removals = %v, want 3 entries", tokenRemovals)
}
if got := stderr.String(); !strings.Contains(got, "Configuration removed") {
t.Fatalf("expected success message on stderr, got %q", got)
}
}
func TestConfigRemoveRun_UsesInjectedCallbacksAndSavesBeforeCleanup(t *testing.T) {
f, _, stderr, _ := cmdutil.TestFactory(t, nil)
cfg := &core.MultiAppConfig{
Apps: []core.AppConfig{
{
AppId: "cli_app_a",
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app_a"}},
Users: []core.AppUser{{UserOpenId: "ou_a"}},
},
{
AppId: "cli_app_b",
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app_b"}},
Users: []core.AppUser{{UserOpenId: "ou_b1"}, {UserOpenId: "ou_b2"}},
},
},
}
callOrder := []string{}
saveConfig := func(next *core.MultiAppConfig) error {
callOrder = append(callOrder, "save")
if len(next.Apps) != 0 {
t.Fatalf("expected empty config, got %+v", next.Apps)
}
return nil
}
var secretRemovals []string
removeSecret := func(input core.SecretInput, kc keychain.KeychainAccess) {
callOrder = append(callOrder, "secret")
if input.Ref != nil {
secretRemovals = append(secretRemovals, input.Ref.ID)
}
}
var tokenRemovals []string
removeStoredToken := func(appID, userOpenID string) error {
callOrder = append(callOrder, "token")
tokenRemovals = append(tokenRemovals, appID+":"+userOpenID)
return nil
}
if err := configRemoveRun(&ConfigRemoveOptions{
Factory: f,
LoadConfig: func() (*core.MultiAppConfig, error) { return cfg, nil },
SaveConfig: saveConfig,
RemoveSecret: removeSecret,
RemoveStoredToken: removeStoredToken,
}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(callOrder) == 0 || callOrder[0] != "save" {
t.Fatalf("expected save to happen first, order=%v", callOrder)
}
if len(secretRemovals) != 2 {
t.Fatalf("secret removals = %v, want 2 entries", secretRemovals)
}
if len(tokenRemovals) != 3 {
t.Fatalf("token removals = %v, want 3 entries", tokenRemovals)
}
if got := stderr.String(); !strings.Contains(got, "Configuration removed") {
t.Fatalf("expected success message on stderr, got %q", got)
}
}
func TestConfigRemoveRun_SaveFailureDoesNotClearSecretsOrTokens(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, nil)
cleanupSecretCalls := 0
cleanupTokenCalls := 0
err := configRemoveRun(&ConfigRemoveOptions{
Factory: f,
LoadConfig: func() (*core.MultiAppConfig, error) {
return &core.MultiAppConfig{
Apps: []core.AppConfig{{
AppId: "cli_app",
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:cli_app"}},
Users: []core.AppUser{{UserOpenId: "ou_1"}},
}},
}, nil
},
SaveConfig: func(*core.MultiAppConfig) error { return errors.New("disk write failed") },
RemoveSecret: func(core.SecretInput, keychain.KeychainAccess) {
cleanupSecretCalls++
},
RemoveStoredToken: func(string, string) error {
cleanupTokenCalls++
return nil
},
})
if err == nil {
t.Fatal("expected save failure")
}
if cleanupSecretCalls != 0 || cleanupTokenCalls != 0 {
t.Fatalf("cleanup must not run when save fails; secret=%d token=%d", cleanupSecretCalls, cleanupTokenCalls)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/config/remove_test.go` around lines 16 - 80, Add a new test that covers
the failure path of SaveConfig to ensure RemoveSecret and RemoveStoredToken are
not invoked when saving fails: duplicate the pattern from
TestConfigRemoveRun_UsesInjectedCallbacksAndSavesBeforeCleanup but make
SaveConfig return a non-nil error (and record callOrder), provide stub
RemoveSecret and RemoveStoredToken that append to slices or callOrder if
invoked, call configRemoveRun with these injected callbacks, assert
configRemoveRun returns the SaveConfig error, verify callOrder contains "save"
as first entry and that no "secret" or "token" entries were appended, and check
stderr contains an appropriate failure message; reference the existing test name
and the configRemoveRun, SaveConfig, RemoveSecret, RemoveStoredToken symbols to
place this new test alongside the current one.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/config/remove.go`:
- Around line 67-73: Resolve the unresolved merge conflict markers by removing
the conflict lines and keeping the fix branch change that calls
opts.SaveConfig(empty) (i.e., use ConfigRemoveOptions' method SaveConfig)
instead of core.SaveMultiAppConfig; delete the <<<<<<<, =======, and >>>>>>>
markers and ensure the conditional error check remains using
opts.SaveConfig(empty) with the existing output.Errorf call so the code compiles
and uses the injected SaveConfig on ConfigRemoveOptions.
- Around line 79-86: Remove the git conflict markers and restore the warning-log
behavior from the fix branch: replace the discarded call to
auth.RemoveStoredToken with the opts.RemoveStoredToken call that logs failures
using fmt.Fprintf to f.IOStreams.ErrOut; ensure the block uses if err :=
opts.RemoveStoredToken(app.AppId, user.UserOpenId); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "warning: failed to remove stored token for app
%q user %q: %v\n", app.AppId, user.UserOpenId, err) } and remove the <<<<<<<,
=======, >>>>>>> markers so the file compiles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 331e78d6-dff2-41c6-bffe-05df9b42a39f

📥 Commits

Reviewing files that changed from the base of the PR and between 7323a70 and 7d27c4c.

📒 Files selected for processing (1)
  • cmd/config/remove.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/config/remove.go`:
- Around line 75-77: The warning currently prints a persistent user identifier
(user.UserOpenId) to stderr via fmt.Fprintf in the RemoveStoredToken error path;
change the message to avoid exposing the raw identifier by either omitting
user.UserOpenId entirely or printing a redacted/truncated form (e.g., show only
a fixed prefix/suffix or replace with "<redacted>") while keeping app.AppId and
the error; update the call that writes to f.IOStreams.ErrOut (the fmt.Fprintf
invocation) so it no longer emits the full user.UserOpenId.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 961c7521-00cf-4460-94ff-c2395a90bcc2

📥 Commits

Reviewing files that changed from the base of the PR and between 7d27c4c and 678f70f.

📒 Files selected for processing (1)
  • cmd/config/remove.go

@zfaustk
Copy link
Copy Markdown
Author

zfaustk commented Apr 12, 2026

Addressed the remaining review concern in the latest two commits.\n\n- removed the raw value from the token-cleanup warning\n- kept the warning actionable by retaining the app id and underlying error\n- updated so the warning assertion no longer expects or allows the user identifier to appear\n\nAt this point the code-side cleanup for this review thread should be complete; the remaining blocker should be maintainer re-review and workflow approval.\n

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@undefined`:
- Around line 75-77: The warning path around opts.RemoveStoredToken(app.AppId,
user.UserOpenId) can leave the final footer "Cleared tokens for %d users"
inaccurate; update the logic to track removal outcomes (e.g., increment success
and failure counters when RemoveStoredToken returns nil vs error) and use those
counters in the final summary so it prints a partial-success message like
"Cleared tokens for X users; failed to clear Y users" (and keep writing warnings
to f.IOStreams.ErrOut as now) instead of always claiming all tokens were
cleared.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40f9e857-252a-4e15-bf0a-727aaad29e13

📥 Commits

Reviewing files that changed from the base of the PR and between 678f70f and 658a41d.

📒 Files selected for processing (1)
  • undefined

Comment on lines +75 to +77
if err := opts.RemoveStoredToken(app.AppId, user.UserOpenId); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "warning: failed to remove a stored token for app %q: %v\n", app.AppId, err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t claim all tokens were cleared after warning paths.

After making RemoveStoredToken failures non-fatal here, the later Cleared tokens for %d users footer can be inaccurate whenever any warning is emitted. Track failed removals and make the summary reflect partial success.

✏️ Possible tweak
+	failedTokenRemovals := 0
+
 	// Clean up keychain entries for all apps after config is cleared.
 	for _, app := range config.Apps {
 		opts.RemoveSecret(app.AppSecret, f.Keychain)
 		for _, user := range app.Users {
 			if err := opts.RemoveStoredToken(app.AppId, user.UserOpenId); err != nil {
+				failedTokenRemovals++
 				fmt.Fprintf(f.IOStreams.ErrOut, "warning: failed to remove a stored token for app %q: %v\n", app.AppId, err)
 			}
 		}
 	}
@@
 	if userCount > 0 {
-		fmt.Fprintf(f.IOStreams.ErrOut, "Cleared tokens for %d users\n", userCount)
+		if failedTokenRemovals == 0 {
+			fmt.Fprintf(f.IOStreams.ErrOut, "Cleared tokens for %d users\n", userCount)
+		} else {
+			fmt.Fprintf(f.IOStreams.ErrOut, "Attempted to clear tokens for %d users (%d failed)\n", userCount, failedTokenRemovals)
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@undefined` around lines 75 - 77, The warning path around
opts.RemoveStoredToken(app.AppId, user.UserOpenId) can leave the final footer
"Cleared tokens for %d users" inaccurate; update the logic to track removal
outcomes (e.g., increment success and failure counters when RemoveStoredToken
returns nil vs error) and use those counters in the final summary so it prints a
partial-success message like "Cleared tokens for X users; failed to clear Y
users" (and keep writing warnings to f.IOStreams.ErrOut as now) instead of
always claiming all tokens were cleared.

Comment on lines +1 to +6
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package config

import (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Test file placed in wrong directory — package conflict

This file declares package config but lives at the repository root alongside main.go (package main). Go does not allow two package names in the same directory even for _test.go files unless the test package suffix (_test) is used. Running any root-level go test or build will produce a "found packages main and config" error.

This file should be placed in cmd/config/remove_test.go (next to cmd/config/remove.go) so it belongs to the correct package directory.

@zfaustk
Copy link
Copy Markdown
Author

zfaustk commented Apr 12, 2026

Follow-up clarification: the latest commits update cmd/config/remove.go and cmd/config/remove_test.go.

The warning now avoids printing the raw user identifier, and the focused test now checks that the identifier is not leaked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large or sensitive change across domains or core paths

Projects

None yet

4 participants