|
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) { |
@@ -454,3 +457,110 @@ command = "" |
454 | 457 | t.Errorf("AI.Command = %q, want empty (explicit disable)", cfg.AI.Command) |
455 | 458 | } |
456 | 459 | } |
| 460 | + |
| 461 | +func TestUseKeyring(t *testing.T) { |
| 462 | + tests := []struct { |
| 463 | + password string |
| 464 | + want bool |
| 465 | + }{ |
| 466 | + {"keyring", true}, |
| 467 | + {"actual-password", false}, |
| 468 | + {"", false}, |
| 469 | + {"$ENV_VAR", false}, |
| 470 | + {"keyring-like", false}, |
| 471 | + } |
| 472 | + for _, tt := range tests { |
| 473 | + acc := AccountConfig{Password: tt.password} |
| 474 | + if got := acc.UseKeyring(); got != tt.want { |
| 475 | + t.Errorf("UseKeyring(%q) = %v, want %v", tt.password, got, tt.want) |
| 476 | + } |
| 477 | + } |
| 478 | +} |
| 479 | + |
| 480 | +func TestResolveKeyringPassword(t *testing.T) { |
| 481 | + // Use the in-memory mock provided by zalando/go-keyring so tests don't |
| 482 | + // touch the real OS keyring. |
| 483 | + zalandokeyring.MockInit() |
| 484 | + |
| 485 | + t.Run("resolved when entry exists", func(t *testing.T) { |
| 486 | + const acct = "TestAcctResolved" |
| 487 | + if err := keyring.SetPassword(acct, "real-secret"); err != nil { |
| 488 | + t.Fatalf("SetPassword: %v", err) |
| 489 | + } |
| 490 | + got := resolveKeyringPassword(acct, "keyring") |
| 491 | + if got != "real-secret" { |
| 492 | + t.Errorf("got %q, want resolved password", got) |
| 493 | + } |
| 494 | + _ = keyring.DeletePassword(acct) |
| 495 | + }) |
| 496 | + |
| 497 | + t.Run("sentinel preserved when entry missing", func(t *testing.T) { |
| 498 | + got := resolveKeyringPassword("MissingAcct", "keyring") |
| 499 | + if got != "keyring" { |
| 500 | + t.Errorf("got %q, want sentinel preserved", got) |
| 501 | + } |
| 502 | + }) |
| 503 | + |
| 504 | + t.Run("non-sentinel passthrough", func(t *testing.T) { |
| 505 | + got := resolveKeyringPassword("any", "literal-password") |
| 506 | + if got != "literal-password" { |
| 507 | + t.Errorf("got %q, want passthrough", got) |
| 508 | + } |
| 509 | + }) |
| 510 | + |
| 511 | + t.Run("empty account name passthrough", func(t *testing.T) { |
| 512 | + // Anonymous accounts (empty Name) should not trigger a keyring lookup. |
| 513 | + got := resolveKeyringPassword("", "keyring") |
| 514 | + if got != "keyring" { |
| 515 | + t.Errorf("got %q, want passthrough for empty account", got) |
| 516 | + } |
| 517 | + }) |
| 518 | +} |
| 519 | + |
| 520 | +// TestLoad_KeyringResolvesAcrossSenders verifies the senders-gap fix: |
| 521 | +// PR #5 resolved keyring at IMAP construction time, leaving SMTP send paths |
| 522 | +// reading the literal "keyring" sentinel. By moving resolution into Load(), |
| 523 | +// every consumer (IMAP, SMTP, [[senders]] aliases) sees the resolved value. |
| 524 | +func TestLoad_KeyringResolvesAcrossSenders(t *testing.T) { |
| 525 | + zalandokeyring.MockInit() |
| 526 | + const acctName = "Personal" |
| 527 | + const realPw = "the-real-password" |
| 528 | + if err := keyring.SetPassword(acctName, realPw); err != nil { |
| 529 | + t.Fatalf("SetPassword: %v", err) |
| 530 | + } |
| 531 | + defer keyring.DeletePassword(acctName) |
| 532 | + |
| 533 | + dir := t.TempDir() |
| 534 | + cfgPath := filepath.Join(dir, "config.toml") |
| 535 | + cfgBody := ` |
| 536 | +[[accounts]] |
| 537 | +name = "Personal" |
| 538 | +imap = "imap.example.com:993" |
| 539 | +smtp = "smtp.example.com:587" |
| 540 | +user = "me@example.com" |
| 541 | +password = "keyring" |
| 542 | +from = "Me <me@example.com>" |
| 543 | +
|
| 544 | +[[senders]] |
| 545 | +name = "Alias" |
| 546 | +from = "Alias <alias@example.com>" |
| 547 | +account = "Personal" |
| 548 | +` |
| 549 | + if err := os.WriteFile(cfgPath, []byte(cfgBody), 0600); err != nil { |
| 550 | + t.Fatalf("write config: %v", err) |
| 551 | + } |
| 552 | + |
| 553 | + cfg, err := Load(cfgPath) |
| 554 | + if err != nil { |
| 555 | + t.Fatalf("Load: %v", err) |
| 556 | + } |
| 557 | + if len(cfg.Accounts) != 1 { |
| 558 | + t.Fatalf("expected 1 account, got %d", len(cfg.Accounts)) |
| 559 | + } |
| 560 | + if got := cfg.Accounts[0].Password; got != realPw { |
| 561 | + t.Errorf("Accounts[0].Password = %q, want resolved %q (the senders-gap fix)", got, realPw) |
| 562 | + } |
| 563 | + if cfg.Accounts[0].UseKeyring() { |
| 564 | + t.Error("UseKeyring() should be false after successful resolution") |
| 565 | + } |
| 566 | +} |
0 commit comments