// Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT package profile import ( "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/i18n" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/vfs" ) type failRenameFS struct { vfs.OsFs err error } func (fs *failRenameFS) Rename(oldpath, newpath string) error { return fs.err } func setupProfileConfigDir(t *testing.T) string { t.Helper() dir := t.TempDir() t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir) return dir } func TestProfileAddRun_InvalidExistingConfigReturnsError(t *testing.T) { dir := setupProfileConfigDir(t) if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte("{invalid json"), 0600); err != nil { t.Fatalf("WriteFile() error = %v", err) } f, _, _, _ := cmdutil.TestFactory(t, nil) f.IOStreams.In = strings.NewReader("secret\n") err := profileAddRun(f, "test", "app-test", true, "feishu", "zh", false) if err == nil { t.Fatal("expected error for invalid existing config") } if !strings.Contains(err.Error(), "failed to load config") { t.Fatalf("error = %v, want failed to load config", err) } } // TestProfileAddRun_Lang covers the unified --lang contract on profile add: // short codes and Feishu locales both canonicalize to the same stored locale, // empty stores no preference, and an unrecognized value errors. func TestProfileAddRun_Lang(t *testing.T) { t.Run("short and locale canonicalize and persist alike", func(t *testing.T) { for _, in := range []string{"ja", "ja_jp"} { setupProfileConfigDir(t) f, _, _, _ := cmdutil.TestFactory(t, nil) f.IOStreams.In = strings.NewReader("secret\n") if err := profileAddRun(f, "p", "app-p", true, "feishu", in, false); err != nil { t.Fatalf("--lang %q: profileAddRun() error = %v", in, err) } saved, err := core.LoadMultiAppConfig() if err != nil { t.Fatalf("LoadMultiAppConfig() error = %v", err) } if app := saved.FindApp("p"); app == nil || app.Lang != i18n.LangJaJP { t.Errorf("--lang %q: stored Lang = %v, want %q", in, app, i18n.LangJaJP) } } }) t.Run("empty stores no preference", func(t *testing.T) { setupProfileConfigDir(t) f, _, _, _ := cmdutil.TestFactory(t, nil) f.IOStreams.In = strings.NewReader("secret\n") if err := profileAddRun(f, "p", "app-p", true, "feishu", "", false); err != nil { t.Fatalf("profileAddRun() error = %v", err) } saved, _ := core.LoadMultiAppConfig() if app := saved.FindApp("p"); app == nil || app.Lang != "" { t.Errorf("stored Lang = %v, want \"\" (unset)", app) } }) t.Run("invalid lang errors", func(t *testing.T) { setupProfileConfigDir(t) f, _, _, _ := cmdutil.TestFactory(t, nil) f.IOStreams.In = strings.NewReader("secret\n") err := profileAddRun(f, "p", "app-p", true, "feishu", "ZH", false) if err == nil { t.Fatal("expected validation error for --lang ZH, got nil") } exitErr, ok := err.(*output.ExitError) if !ok || exitErr.Code != output.ExitValidation { t.Fatalf("expected ExitValidation, got %T: %v", err, err) } }) } func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "default", Apps: []core.AppConfig{ {Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu}, }, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } f, _, _, _ := cmdutil.TestFactory(t, nil) f.IOStreams.In = strings.NewReader("secret-new\n") if err := profileAddRun(f, "target", "app-target", true, "lark", "en", true); err != nil { t.Fatalf("profileAddRun() error = %v", err) } saved, err := core.LoadMultiAppConfig() if err != nil { t.Fatalf("LoadMultiAppConfig() error = %v", err) } if saved.CurrentApp != "target" { t.Fatalf("CurrentApp = %q, want %q", saved.CurrentApp, "target") } if saved.PreviousApp != "default" { t.Fatalf("PreviousApp = %q, want %q", saved.PreviousApp, "default") } if len(saved.Apps) != 2 { t.Fatalf("len(Apps) = %d, want 2", len(saved.Apps)) } } func TestProfileRemoveRun_RemovesCurrentProfileAndSwitchesToFirstRemaining(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "target", PreviousApp: "default", Apps: []core.AppConfig{ {Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu}, {Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark}, }, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } f, _, _, _ := cmdutil.TestFactory(t, nil) if err := profileRemoveRun(f, "target"); err != nil { t.Fatalf("profileRemoveRun() error = %v", err) } saved, err := core.LoadMultiAppConfig() if err != nil { t.Fatalf("LoadMultiAppConfig() error = %v", err) } if saved.CurrentApp != "default" { t.Fatalf("CurrentApp = %q, want %q", saved.CurrentApp, "default") } if saved.PreviousApp != "default" { t.Fatalf("PreviousApp = %q, want %q", saved.PreviousApp, "default") } if len(saved.Apps) != 1 || saved.Apps[0].ProfileName() != "default" { t.Fatalf("remaining apps = %#v, want only default", saved.Apps) } } func TestProfileRenameRun_UpdatesCurrentAndPreviousReferences(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "old", PreviousApp: "old", Apps: []core.AppConfig{{ Name: "old", AppId: "app-old", AppSecret: core.PlainSecret("secret-old"), Brand: core.BrandFeishu, }}, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } f, _, _, _ := cmdutil.TestFactory(t, nil) if err := profileRenameRun(f, "old", "new"); err != nil { t.Fatalf("profileRenameRun() error = %v", err) } saved, err := core.LoadMultiAppConfig() if err != nil { t.Fatalf("LoadMultiAppConfig() error = %v", err) } if saved.CurrentApp != "new" { t.Fatalf("CurrentApp = %q, want %q", saved.CurrentApp, "new") } if saved.PreviousApp != "new" { t.Fatalf("PreviousApp = %q, want %q", saved.PreviousApp, "new") } if saved.Apps[0].ProfileName() != "new" { t.Fatalf("ProfileName() = %q, want %q", saved.Apps[0].ProfileName(), "new") } } func TestProfileRenameRun_AllowsRenameToOwnAppID(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "old", PreviousApp: "old", Apps: []core.AppConfig{{ Name: "old", AppId: "app-old", AppSecret: core.PlainSecret("secret-old"), Brand: core.BrandFeishu, }}, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } f, _, _, _ := cmdutil.TestFactory(t, nil) if err := profileRenameRun(f, "old", "app-old"); err != nil { t.Fatalf("profileRenameRun() error = %v", err) } saved, err := core.LoadMultiAppConfig() if err != nil { t.Fatalf("LoadMultiAppConfig() error = %v", err) } if saved.CurrentApp != "app-old" { t.Fatalf("CurrentApp = %q, want %q", saved.CurrentApp, "app-old") } if saved.PreviousApp != "app-old" { t.Fatalf("PreviousApp = %q, want %q", saved.PreviousApp, "app-old") } if saved.Apps[0].Name != "app-old" { t.Fatalf("Name = %q, want %q", saved.Apps[0].Name, "app-old") } } func TestProfileUseRun_ToggleBackUsesPreviousProfile(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "default", PreviousApp: "target", Apps: []core.AppConfig{ {Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu}, {Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark}, }, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } f, _, _, _ := cmdutil.TestFactory(t, nil) if err := profileUseRun(f, "-"); err != nil { t.Fatalf("profileUseRun() error = %v", err) } saved, err := core.LoadMultiAppConfig() if err != nil { t.Fatalf("LoadMultiAppConfig() error = %v", err) } if saved.CurrentApp != "target" { t.Fatalf("CurrentApp = %q, want %q", saved.CurrentApp, "target") } if saved.PreviousApp != "default" { t.Fatalf("PreviousApp = %q, want %q", saved.PreviousApp, "default") } } func TestProfileListRun_OutputsProfiles(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "default", Apps: []core.AppConfig{ {Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu}, {Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark}, }, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } f, stdout, _, _ := cmdutil.TestFactory(t, nil) if err := profileListRun(f); err != nil { t.Fatalf("profileListRun() error = %v", err) } var got []profileListItem if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("Unmarshal() error = %v; output=%s", err, stdout.String()) } if len(got) != 2 { t.Fatalf("len(got) = %d, want 2", len(got)) } if got[0].Name != "default" || !got[0].Active { t.Fatalf("got[0] = %#v, want active default profile", got[0]) } if got[1].Name != "target" || got[1].Active { t.Fatalf("got[1] = %#v, want inactive target profile", got[1]) } } func TestProfileListRun_NotConfiguredReturnsEmptyList(t *testing.T) { setupProfileConfigDir(t) f, stdout, stderr, _ := cmdutil.TestFactory(t, nil) if err := profileListRun(f); err != nil { t.Fatalf("profileListRun() error = %v", err) } var got []profileListItem if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("Unmarshal() error = %v; output=%s", err, stdout.String()) } if len(got) != 0 { t.Fatalf("len(got) = %d, want 0", len(got)) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestProfileRemoveRun_SaveFailureReturnsStructuredError(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "target", Apps: []core.AppConfig{ {Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu}, {Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark}, }, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } restoreFS := vfs.DefaultFS vfs.DefaultFS = &failRenameFS{err: errors.New("rename boom")} t.Cleanup(func() { vfs.DefaultFS = restoreFS }) f, _, _, _ := cmdutil.TestFactory(t, nil) err := profileRemoveRun(f, "target") if err == nil { t.Fatal("expected save error") } assertInternalExitError(t, err, "failed to save config") } func TestProfileRenameRun_SaveFailureReturnsStructuredError(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "old", Apps: []core.AppConfig{{ Name: "old", AppId: "app-old", AppSecret: core.PlainSecret("secret-old"), Brand: core.BrandFeishu, }}, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } restoreFS := vfs.DefaultFS vfs.DefaultFS = &failRenameFS{err: errors.New("rename boom")} t.Cleanup(func() { vfs.DefaultFS = restoreFS }) f, _, _, _ := cmdutil.TestFactory(t, nil) err := profileRenameRun(f, "old", "new") if err == nil { t.Fatal("expected save error") } assertInternalExitError(t, err, "failed to save config") } func TestProfileUseRun_SaveFailureReturnsStructuredError(t *testing.T) { setupProfileConfigDir(t) multi := &core.MultiAppConfig{ CurrentApp: "default", Apps: []core.AppConfig{ {Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu}, {Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark}, }, } if err := core.SaveMultiAppConfig(multi); err != nil { t.Fatalf("SaveMultiAppConfig() error = %v", err) } restoreFS := vfs.DefaultFS vfs.DefaultFS = &failRenameFS{err: errors.New("rename boom")} t.Cleanup(func() { vfs.DefaultFS = restoreFS }) f, _, _, _ := cmdutil.TestFactory(t, nil) err := profileUseRun(f, "target") if err == nil { t.Fatal("expected save error") } assertInternalExitError(t, err, "failed to save config") } func assertInternalExitError(t *testing.T, err error, wantMsg string) { t.Helper() var exitErr *output.ExitError if !errors.As(err, &exitErr) { t.Fatalf("error type = %T, want *output.ExitError; err=%v", err, err) } if exitErr.Code != output.ExitInternal { t.Fatalf("exit code = %d, want %d", exitErr.Code, output.ExitInternal) } if exitErr.Detail == nil || exitErr.Detail.Type != "internal" { t.Fatalf("detail = %#v, want internal detail", exitErr.Detail) } if !strings.Contains(exitErr.Detail.Message, wantMsg) { t.Fatalf("message = %q, want contains %q", exitErr.Detail.Message, wantMsg) } }