package secretstore import ( "errors" "fmt" "io" "os" "slices" "testing" ) func TestLoginBitwardenRunsInteractiveFlowAndPersistsSession(t *testing.T) { t.Setenv("BW_SESSION", "") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) { if len(args) == 1 && args[0] == "status" { return []byte(`{"status":"unauthenticated"}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) }) var calls [][]string withBitwardenInteractiveRunner(t, func(command string, stdin io.Reader, stdout, stderr io.Writer, args ...string) ([]byte, error) { calls = append(calls, slices.Clone(args)) switch { case len(args) == 1 && args[0] == "login": return nil, nil case len(args) == 2 && args[0] == "unlock" && args[1] == "--raw": return []byte("persisted-session\n"), nil default: return nil, fmt.Errorf("unexpected interactive args: %v", args) } }) session, err := LoginBitwarden(BitwardenLoginOptions{ ServiceName: "email-mcp", BitwardenCommand: "bw", }) if err != nil { t.Fatalf("LoginBitwarden returned error: %v", err) } if session != "persisted-session" { t.Fatalf("session = %q, want persisted-session", session) } if got := os.Getenv("BW_SESSION"); got != "persisted-session" { t.Fatalf("BW_SESSION = %q, want persisted-session", got) } if len(calls) != 2 { t.Fatalf("interactive call count = %d, want 2", len(calls)) } if !slices.Equal(calls[0], []string{"login"}) { t.Fatalf("interactive call #1 args = %v, want [login]", calls[0]) } if !slices.Equal(calls[1], []string{"unlock", "--raw"}) { t.Fatalf("interactive call #2 args = %v, want [unlock --raw]", calls[1]) } persisted, err := LoadBitwardenSession(BitwardenSessionOptions{ServiceName: bitwardenSharedSessionName}) if err != nil { t.Fatalf("LoadBitwardenSession (shared) returned error: %v", err) } if persisted != "persisted-session" { t.Fatalf("shared persisted session = %q, want persisted-session", persisted) } } func TestLoginBitwardenSkipsInteractiveLoginWhenAlreadyAuthenticated(t *testing.T) { t.Setenv("BW_SESSION", "") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) { if len(args) == 1 && args[0] == "status" { return []byte(`{"status":"locked"}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) }) var calls [][]string withBitwardenInteractiveRunner(t, func(command string, stdin io.Reader, stdout, stderr io.Writer, args ...string) ([]byte, error) { calls = append(calls, slices.Clone(args)) if len(args) == 2 && args[0] == "unlock" && args[1] == "--raw" { return []byte("session-locked\n"), nil } return nil, fmt.Errorf("unexpected interactive args: %v", args) }) session, err := LoginBitwarden(BitwardenLoginOptions{ ServiceName: "email-mcp", BitwardenCommand: "bw", }) if err != nil { t.Fatalf("LoginBitwarden returned error: %v", err) } if session != "session-locked" { t.Fatalf("session = %q, want session-locked", session) } if len(calls) != 1 { t.Fatalf("interactive call count = %d, want 1", len(calls)) } if !slices.Equal(calls[0], []string{"unlock", "--raw"}) { t.Fatalf("interactive call args = %v, want [unlock --raw]", calls[0]) } } func TestBitwardenSessionEnvLoadsFromPersistedSession(t *testing.T) { t.Setenv("BW_SESSION", "") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) path, err := SaveBitwardenSession(BitwardenSessionOptions{ServiceName: "email-mcp"}, "persisted-session") if err != nil { t.Fatalf("SaveBitwardenSession returned error: %v", err) } info, err := os.Stat(path) if err != nil { t.Fatalf("Stat persisted session file: %v", err) } if got := info.Mode().Perm(); got != 0o600 { t.Fatalf("session file mode = %o, want 600", got) } loaded, err := EnsureBitwardenSessionEnv(BitwardenSessionOptions{ServiceName: "email-mcp"}) if err != nil { t.Fatalf("EnsureBitwardenSessionEnv returned error: %v", err) } if !loaded { t.Fatal("expected session to be loaded from persisted file") } if got := os.Getenv("BW_SESSION"); got != "persisted-session" { t.Fatalf("BW_SESSION = %q, want persisted-session", got) } } func TestBitwardenSessionEnvDoesNotOverrideExistingValue(t *testing.T) { t.Setenv("BW_SESSION", "from-env") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) if _, err := SaveBitwardenSession(BitwardenSessionOptions{ServiceName: "email-mcp"}, "persisted-session"); err != nil { t.Fatalf("SaveBitwardenSession returned error: %v", err) } loaded, err := EnsureBitwardenSessionEnv(BitwardenSessionOptions{ServiceName: "email-mcp"}) if err != nil { t.Fatalf("EnsureBitwardenSessionEnv returned error: %v", err) } if loaded { t.Fatal("expected existing BW_SESSION to be kept") } if got := os.Getenv("BW_SESSION"); got != "from-env" { t.Fatalf("BW_SESSION = %q, want from-env", got) } } func TestOpenBitwardenCLILoadsPersistedSessionWhenEnvironmentIsMissing(t *testing.T) { t.Setenv("BW_SESSION", "") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) if _, err := SaveBitwardenSession(BitwardenSessionOptions{ServiceName: "email-mcp"}, "persisted-open-session"); err != nil { t.Fatalf("SaveBitwardenSession returned error: %v", err) } fakeCLI := newFakeBitwardenCLI("bw") withBitwardenRunner(t, fakeCLI.run) store, err := Open(Options{ ServiceName: "email-mcp", BackendPolicy: BackendBitwardenCLI, }) if err != nil { t.Fatalf("Open returned error: %v", err) } if _, ok := store.(*bitwardenStore); !ok { t.Fatalf("store type = %T, want *bitwardenStore", store) } if got := os.Getenv("BW_SESSION"); got != "persisted-open-session" { t.Fatalf("BW_SESSION = %q, want persisted-open-session", got) } } func TestLoadBitwardenSessionReturnsNotFoundWhenFileMissing(t *testing.T) { configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) _, err := LoadBitwardenSession(BitwardenSessionOptions{ServiceName: "email-mcp"}) if !errors.Is(err, ErrNotFound) { t.Fatalf("error = %v, want ErrNotFound", err) } } func TestLoginBitwardenPersistsToSharedFileAfterUnlock(t *testing.T) { t.Setenv("BW_SESSION", "") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) { if len(args) == 1 && args[0] == "status" { return []byte(`{"status":"locked"}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) }) withBitwardenInteractiveRunner(t, func(command string, stdin io.Reader, stdout, stderr io.Writer, args ...string) ([]byte, error) { if len(args) == 2 && args[0] == "unlock" && args[1] == "--raw" { return []byte("shared-session\n"), nil } return nil, fmt.Errorf("unexpected interactive args: %v", args) }) if _, err := LoginBitwarden(BitwardenLoginOptions{ ServiceName: "graylog-mcp", BitwardenCommand: "bw", }); err != nil { t.Fatalf("LoginBitwarden returned error: %v", err) } shared, err := LoadBitwardenSession(BitwardenSessionOptions{ServiceName: bitwardenSharedSessionName}) if err != nil { t.Fatalf("LoadBitwardenSession (shared) returned error: %v", err) } if shared != "shared-session" { t.Fatalf("shared session = %q, want shared-session", shared) } } func TestLoginBitwardenSkipsUnlockWhenAlreadyUnlockedWithEnvSession(t *testing.T) { t.Setenv("BW_SESSION", "existing-session") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) { if len(args) == 1 && args[0] == "status" { return []byte(`{"status":"unlocked"}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) }) withBitwardenInteractiveRunner(t, func(command string, stdin io.Reader, stdout, stderr io.Writer, args ...string) ([]byte, error) { return nil, fmt.Errorf("bw unlock must not be called when vault is already unlocked with a session: %v", args) }) session, err := LoginBitwarden(BitwardenLoginOptions{ ServiceName: "email-mcp", BitwardenCommand: "bw", }) if err != nil { t.Fatalf("LoginBitwarden returned error: %v", err) } if session != "existing-session" { t.Fatalf("session = %q, want existing-session", session) } } func TestLoginBitwardenSkipsUnlockWhenAlreadyUnlockedWithSharedSession(t *testing.T) { t.Setenv("BW_SESSION", "") configDir := t.TempDir() withBitwardenUserConfigDir(t, func() (string, error) { return configDir, nil }) // graylog-mcp logged in earlier and wrote to the shared file if _, err := SaveBitwardenSession(BitwardenSessionOptions{ServiceName: bitwardenSharedSessionName}, "graylog-session"); err != nil { t.Fatalf("SaveBitwardenSession returned error: %v", err) } withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) { if len(args) == 1 && args[0] == "status" { return []byte(`{"status":"unlocked"}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) }) withBitwardenInteractiveRunner(t, func(command string, stdin io.Reader, stdout, stderr io.Writer, args ...string) ([]byte, error) { return nil, fmt.Errorf("bw unlock must not be called when shared session exists: %v", args) }) session, err := LoginBitwarden(BitwardenLoginOptions{ ServiceName: "email-mcp", BitwardenCommand: "bw", }) if err != nil { t.Fatalf("LoginBitwarden returned error: %v", err) } if session != "graylog-session" { t.Fatalf("session = %q, want graylog-session (from shared file)", session) } // The shared session must also be persisted to the service-specific file persisted, err := LoadBitwardenSession(BitwardenSessionOptions{ServiceName: "email-mcp"}) if err != nil { t.Fatalf("LoadBitwardenSession returned error: %v", err) } if persisted != "graylog-session" { t.Fatalf("email-mcp persisted session = %q, want graylog-session", persisted) } } func withBitwardenInteractiveRunner( t *testing.T, runner func(command string, stdin io.Reader, stdout, stderr io.Writer, args ...string) ([]byte, error), ) { t.Helper() previous := runBitwardenInteractiveCLI runBitwardenInteractiveCLI = runner t.Cleanup(func() { runBitwardenInteractiveCLI = previous }) } func withBitwardenUserConfigDir(t *testing.T, resolver func() (string, error)) { t.Helper() previous := bitwardenUserConfigDir bitwardenUserConfigDir = resolver t.Cleanup(func() { bitwardenUserConfigDir = previous }) }