mcp-framework/secretstore/bitwarden_session_test.go
thibaud-lclr 7c016e8c5e refactor(secretstore): supprimer le fichier de session service-spécifique
Le fichier ~/.config/<service>/bw-session est redondant depuis l'introduction
du fichier partagé mcp-framework. On n'écrit plus que dans le partagé et on lit
uniquement depuis lui dans refreshSessionEnv et loadAnyBitwardenSession.
EnsureBitwardenSessionEnv tente le fichier service-spécifique en premier
(rétrocompat) puis bascule sur le partagé.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:57:58 +02:00

337 lines
11 KiB
Go

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