# Hostkeeper Test Plan — Phase 3 > **Created**: 2025-01-31 > **Purpose**: Comprehensive test coverage for all packages > **Target Coverage**: 90%+ for all packages --- ## Current Coverage Summary | Package | Current | Target | Gap | |---------|---------|--------|-----| | `internal/errors` | 100% | 100% | — | | `internal/models` | 91% | 100% | +9% | | `pkg/crypto` | **0%** | **100%** | +100% | | `pkg/knownhosts` | **0%** | **100%** | +100% | | `pkg/ssh` | 60% | 90% | +30% | | `pkg/storage` | 40% | 90% | +50% | | `pkg/config` | 22% | 80% | +58% | | `pkg/tui` | ~40% | 70% | +30% | | `cmd/hostkeeper` | ~40% | 70% | +30% | --- ## Test File Structure ``` test/ ├── crypto/ │ └── crypto_test.go (NEW — 17 scenarios) ├── knownhosts/ │ └── knownhosts_test.go (NEW — 14 scenarios) ├── storage/ │ ├── export_import_test.go (EXISTING) │ └── json_storage_test.go (NEW — 18 scenarios) ├── config/ │ └── config_test.go (NEW — 10 scenarios) ├── ssh/ │ └── ssh_test.go (EXISTING) ├── tui/ │ ├── theme_test.go (EXISTING) │ ├── error_banner_test.go (EXISTING) │ └── responsive_test.go (NEW — 10 scenarios) ├── cmd/ │ └── root_test.go (NEW — 8 scenarios) ├── errors_test.go (EXISTING) ├── models_test.go (EXISTING) └── integration/ └── integration_test.go (EXISTING) ``` **Total: 7 new test files, ~87 test scenarios** --- ## Scenario Details ### 1. `test/crypto/crypto_test.go` — CRITICAL | # | Scenario | Input | Expected | Priority | |---|----------|-------|----------|----------| | 1.1 | DeriveKey determinism | same password + salt | same key | HIGH | | 1.2 | DeriveKey password variation | different password | different key | HIGH | | 1.3 | DeriveKey salt variation | different salt | different key | HIGH | | 1.4 | DeriveKey empty password | "" | no panic, valid key | MEDIUM | | 1.5 | Encrypt/Decrypt round-trip | "hello world" | decrypt = original | HIGH | | 1.6 | Encrypt empty plaintext | "" | decrypt = "" | MEDIUM | | 1.7 | Encrypt large data | 1MB random bytes | round-trip OK | MEDIUM | | 1.8 | Encrypt unicode | "こんにちは" | round-trip OK | MEDIUM | | 1.9 | Encrypt with newlines | "line1\nline2" | round-trip OK | MEDIUM | | 1.10 | Wrong password | decrypt with wrong pw | ErrDecryptionFailed | HIGH | | 1.11 | Empty password | decrypt with "" | ErrDecryptionFailed | HIGH | | 1.12 | IsEncrypted valid ciphertext | base64 ciphertext | true | HIGH | | 1.13 | IsEncrypted plaintext | "hello" | false | HIGH | | 1.14 | IsEncrypted empty | "" | false | MEDIUM | | 1.15 | HashPassword determinism | same pw | same hash | HIGH | | 1.16 | HashPassword variation | different pw | different hash | HIGH | | 1.17 | Encrypt randomness | same input, 2 calls | different ciphertext | MEDIUM | ### 2. `test/knownhosts/knownhosts_test.go` — CRITICAL | # | Scenario | Input | Expected | Priority | |---|----------|-------|----------|----------| | 2.1 | New creates file | non-existent path | file created | HIGH | | 2.2 | New loads existing | existing file | hosts loaded | HIGH | | 2.3 | Add new host | hostname+port+key | added | HIGH | | 2.4 | Add duplicate | same host twice | no error, no duplicate | HIGH | | 2.5 | Get existing | hostname+port | returns HostKey | HIGH | | 2.6 | Get non-existent | unknown host | nil | MEDIUM | | 2.7 | Remove existing | hostname+port | removed | HIGH | | 2.8 | Remove non-existent | unknown host | no error | MEDIUM | | 2.9 | Verify unknown | new host | (false, nil) TOFU | HIGH | | 2.10 | Verify known match | correct key | (true, hostKey) | HIGH | | 2.11 | Verify known mismatch | wrong key | (false, hostKey) MITM | HIGH | | 2.12 | HostKeyCallback | autoAdd=true | adds unknown hosts | HIGH | | 2.13 | Persistence | Add → Save → New → Get | found | HIGH | | 2.14 | Corrupted file | invalid JSON | error | MEDIUM | ### 3. `test/storage/json_storage_test.go` — HIGH | # | Scenario | Input | Expected | Priority | |---|----------|-------|----------|----------| | 3.1 | SaveKeyPair | valid KeyPair | success | HIGH | | 3.2 | ListKeyPairs | after save | returns saved | HIGH | | 3.3 | GetKeyPair found | by ID | returns KeyPair | HIGH | | 3.4 | GetKeyPair not found | unknown ID | error | MEDIUM | | 3.5 | DeleteKeyPair | by ID | removed | HIGH | | 3.6 | DeleteKeyPair not found | unknown ID | no error | MEDIUM | | 3.7 | SaveSnippet | valid Snippet | success | HIGH | | 3.8 | ListSnippets | after save | returns saved | HIGH | | 3.9 | GetSnippet found | by ID | returns Snippet | HIGH | | 3.10 | GetSnippet not found | unknown ID | error | MEDIUM | | 3.11 | DeleteSnippet | by ID | removed | HIGH | | 3.12 | DeleteSnippet not found | unknown ID | no error | MEDIUM | | 3.13 | SetPassword + SaveHost | encrypted storage | file encrypted | HIGH | | 3.14 | IsDataEncrypted | encrypted file | true | HIGH | | 3.15 | Wrong password load | decrypt with wrong pw | error | HIGH | | 3.16 | MergeStrategyMerge | import with merge | keeps existing + adds new | HIGH | | 3.17 | MergeStrategyReplace | import with replace | overwrites all | HIGH | | 3.18 | SaveHost empty ID | host with "" ID | generates UUID | MEDIUM | ### 4. `test/config/config_test.go` — MEDIUM | # | Scenario | Input | Expected | Priority | |---|----------|-------|----------|----------| | 4.1 | New first run | no config file | creates default | HIGH | | 4.2 | New existing | valid config file | loads config | HIGH | | 4.3 | Save | modify + save | persists | HIGH | | 4.4 | UpdateAppConfig | change theme | saved | HIGH | | 4.5 | GetConfigDir | — | valid path | MEDIUM | | 4.6 | GetDataDir | — | valid path | MEDIUM | | 4.7 | GetConfigFilePath | — | ends with config.json | MEDIUM | | 4.8 | GetHostsFilePath | — | ends with hosts.json | MEDIUM | | 4.9 | GetKeysFilePath | — | ends with keys.json | MEDIUM | | 4.10 | GetSnippetsFilePath | — | ends with snippets.json | MEDIUM | ### 5. `test/tui/responsive_test.go` — MEDIUM | # | Scenario | Input | Expected | Priority | |---|----------|-------|----------|----------| | 5.1 | WrapFooter short | short string | single line | MEDIUM | | 5.2 | WrapFooter long | long string | multi-line | MEDIUM | | 5.3 | WrapFooter empty | "" | "" | LOW | | 5.4 | ClampWidth over | width > max | clamped to max | MEDIUM | | 5.5 | ClampWidth under | width < min | clamped to min | MEDIUM | | 5.6 | ClampWidth in range | min < width < max | unchanged | MEDIUM | | 5.7 | TruncateStr short | short string | unchanged | MEDIUM | | 5.8 | TruncateStr long | long string | truncated + … | MEDIUM | | 5.9 | TruncateStr empty | "" | "" | LOW | | 5.10 | TruncateStr unicode | "こんにちは世界" | correct width | MEDIUM | ### 6. `test/cmd/root_test.go` — MEDIUM | # | Scenario | Input | Expected | Priority | |---|----------|-------|----------|----------| | 6.1 | RootCmd execute | no args | no error | HIGH | | 6.2 | Version flag | --version | shows version | MEDIUM | | 6.3 | FindHost by ID | host ID | returns host | HIGH | | 6.4 | FindHost by name | host name | returns host | HIGH | | 6.5 | FindHost by hostname | IP/hostname | returns host | HIGH | | 6.6 | FindHost not found | unknown | error | MEDIUM | | 6.7 | NewStorage no password | --password="" | unencrypted | MEDIUM | | 6.8 | NewStorage with password | --password="x" | encrypted | HIGH | --- ## Execution Order ``` Phase 3, Sprint 1: Security-Critical (pkg/crypto, pkg/knownhosts) Phase 3, Sprint 2: Core (pkg/storage, pkg/config) Phase 3, Sprint 3: UI + CLI (pkg/tui, cmd/hostkeeper) Phase 3, Sprint 4: Integration + Final ``` --- ## Success Criteria - [ ] All tests pass (`go test ./test/...`) - [ ] Coverage >= 90% for `pkg/crypto` - [ ] Coverage >= 90% for `pkg/knownhosts` - [ ] Coverage >= 80% for `pkg/storage` - [ ] Coverage >= 70% for `pkg/config` - [ ] Coverage >= 60% for `pkg/tui` - [ ] No race conditions (`go test -race ./test/...`) - [ ] Build clean (`go build ./...`)