|
5 | 5 | "path/filepath" |
6 | 6 | "strings" |
7 | 7 | "testing" |
| 8 | + |
| 9 | + zalandokeyring "github.com/zalando/go-keyring" |
| 10 | + "github.com/sspaeti/neomd/internal/keyring" |
8 | 11 | ) |
9 | 12 |
|
10 | 13 | func TestExpandEnv(t *testing.T) { |
@@ -368,3 +371,110 @@ func TestValidate_NegativeUIValues(t *testing.T) { |
368 | 371 | t.Error("expected error for negative inbox_count") |
369 | 372 | } |
370 | 373 | } |
| 374 | + |
| 375 | +func TestUseKeyring(t *testing.T) { |
| 376 | + tests := []struct { |
| 377 | + password string |
| 378 | + want bool |
| 379 | + }{ |
| 380 | + {"keyring", true}, |
| 381 | + {"actual-password", false}, |
| 382 | + {"", false}, |
| 383 | + {"$ENV_VAR", false}, |
| 384 | + {"keyring-like", false}, |
| 385 | + } |
| 386 | + for _, tt := range tests { |
| 387 | + acc := AccountConfig{Password: tt.password} |
| 388 | + if got := acc.UseKeyring(); got != tt.want { |
| 389 | + t.Errorf("UseKeyring(%q) = %v, want %v", tt.password, got, tt.want) |
| 390 | + } |
| 391 | + } |
| 392 | +} |
| 393 | + |
| 394 | +func TestResolveKeyringPassword(t *testing.T) { |
| 395 | + // Use the in-memory mock provided by zalando/go-keyring so tests don't |
| 396 | + // touch the real OS keyring. |
| 397 | + zalandokeyring.MockInit() |
| 398 | + |
| 399 | + t.Run("resolved when entry exists", func(t *testing.T) { |
| 400 | + const acct = "TestAcctResolved" |
| 401 | + if err := keyring.SetPassword(acct, "real-secret"); err != nil { |
| 402 | + t.Fatalf("SetPassword: %v", err) |
| 403 | + } |
| 404 | + got := resolveKeyringPassword(acct, "keyring") |
| 405 | + if got != "real-secret" { |
| 406 | + t.Errorf("got %q, want resolved password", got) |
| 407 | + } |
| 408 | + _ = keyring.DeletePassword(acct) |
| 409 | + }) |
| 410 | + |
| 411 | + t.Run("sentinel preserved when entry missing", func(t *testing.T) { |
| 412 | + got := resolveKeyringPassword("MissingAcct", "keyring") |
| 413 | + if got != "keyring" { |
| 414 | + t.Errorf("got %q, want sentinel preserved", got) |
| 415 | + } |
| 416 | + }) |
| 417 | + |
| 418 | + t.Run("non-sentinel passthrough", func(t *testing.T) { |
| 419 | + got := resolveKeyringPassword("any", "literal-password") |
| 420 | + if got != "literal-password" { |
| 421 | + t.Errorf("got %q, want passthrough", got) |
| 422 | + } |
| 423 | + }) |
| 424 | + |
| 425 | + t.Run("empty account name passthrough", func(t *testing.T) { |
| 426 | + // Anonymous accounts (empty Name) should not trigger a keyring lookup. |
| 427 | + got := resolveKeyringPassword("", "keyring") |
| 428 | + if got != "keyring" { |
| 429 | + t.Errorf("got %q, want passthrough for empty account", got) |
| 430 | + } |
| 431 | + }) |
| 432 | +} |
| 433 | + |
| 434 | +// TestLoad_KeyringResolvesAcrossSenders verifies the senders-gap fix: |
| 435 | +// PR #5 resolved keyring at IMAP construction time, leaving SMTP send paths |
| 436 | +// reading the literal "keyring" sentinel. By moving resolution into Load(), |
| 437 | +// every consumer (IMAP, SMTP, [[senders]] aliases) sees the resolved value. |
| 438 | +func TestLoad_KeyringResolvesAcrossSenders(t *testing.T) { |
| 439 | + zalandokeyring.MockInit() |
| 440 | + const acctName = "Personal" |
| 441 | + const realPw = "the-real-password" |
| 442 | + if err := keyring.SetPassword(acctName, realPw); err != nil { |
| 443 | + t.Fatalf("SetPassword: %v", err) |
| 444 | + } |
| 445 | + defer keyring.DeletePassword(acctName) |
| 446 | + |
| 447 | + dir := t.TempDir() |
| 448 | + cfgPath := filepath.Join(dir, "config.toml") |
| 449 | + cfgBody := ` |
| 450 | +[[accounts]] |
| 451 | +name = "Personal" |
| 452 | +imap = "imap.example.com:993" |
| 453 | +smtp = "smtp.example.com:587" |
| 454 | +user = "me@example.com" |
| 455 | +password = "keyring" |
| 456 | +from = "Me <me@example.com>" |
| 457 | +
|
| 458 | +[[senders]] |
| 459 | +name = "Alias" |
| 460 | +from = "Alias <alias@example.com>" |
| 461 | +account = "Personal" |
| 462 | +` |
| 463 | + if err := os.WriteFile(cfgPath, []byte(cfgBody), 0600); err != nil { |
| 464 | + t.Fatalf("write config: %v", err) |
| 465 | + } |
| 466 | + |
| 467 | + cfg, err := Load(cfgPath) |
| 468 | + if err != nil { |
| 469 | + t.Fatalf("Load: %v", err) |
| 470 | + } |
| 471 | + if len(cfg.Accounts) != 1 { |
| 472 | + t.Fatalf("expected 1 account, got %d", len(cfg.Accounts)) |
| 473 | + } |
| 474 | + if got := cfg.Accounts[0].Password; got != realPw { |
| 475 | + t.Errorf("Accounts[0].Password = %q, want resolved %q (the senders-gap fix)", got, realPw) |
| 476 | + } |
| 477 | + if cfg.Accounts[0].UseKeyring() { |
| 478 | + t.Error("UseKeyring() should be false after successful resolution") |
| 479 | + } |
| 480 | +} |
0 commit comments