Files
HostKeeper/test/crypto/crypto_test.go
swanadiva 4a8b4a2bc4 test: Phase 3 comprehensive test coverage — 105 tests, zero race conditions
- Add test/crypto/ (19 tests): AES-256-GCM encrypt/decrypt, PBKDF2, IsEncrypted, HashPassword
- Add test/knownhosts/ (14 tests): TOFU verify, MITM detection, CRUD, persistence
- Add test/storage/ (20 tests): KeyPair/Snippet CRUD, encryption, MergeStrategy
- Add test/config/ (9 tests): config lifecycle, path getters
- Add test/tui/ (10 tests): WrapFooter, ClampWidth, TruncateStr
- Fix knownhosts deadlock: Add/Remove use saveInternal()
- Export responsive.go functions for testing
- Add docs/TEST_PLAN.md with full scenario documentation

Coverage: crypto 0%→100%, knownhosts 0%→100%, storage 40%→90%, config 22%→80%, tui 40%→70%
2026-06-29 12:05:02 +07:00

237 lines
6.7 KiB
Go

package crypto_test
import (
"bytes"
"crypto/rand"
"strings"
"testing"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/crypto"
)
// 1.1 DeriveKey determinism — same password + salt → same key
func TestDeriveKeyDeterminism(t *testing.T) {
salt := []byte("1234567890123456")
key1 := crypto.DeriveKey("password", salt)
key2 := crypto.DeriveKey("password", salt)
if !bytes.Equal(key1, key2) {
t.Error("DeriveKey should return same key for same password + salt")
}
}
// 1.2 DeriveKey password variation — different password → different key
func TestDeriveKeyPasswordVariation(t *testing.T) {
salt := []byte("1234567890123456")
key1 := crypto.DeriveKey("password1", salt)
key2 := crypto.DeriveKey("password2", salt)
if bytes.Equal(key1, key2) {
t.Error("DeriveKey should return different keys for different passwords")
}
}
// 1.3 DeriveKey salt variation — different salt → different key
func TestDeriveKeySaltVariation(t *testing.T) {
key1 := crypto.DeriveKey("password", []byte("1234567890123456"))
key2 := crypto.DeriveKey("password", []byte("6543210987654321"))
if bytes.Equal(key1, key2) {
t.Error("DeriveKey should return different keys for different salts")
}
}
// 1.4 DeriveKey empty password — no panic, valid key length
func TestDeriveKeyEmptyPassword(t *testing.T) {
salt := []byte("1234567890123456")
key := crypto.DeriveKey("", salt)
if len(key) != crypto.KeyLength {
t.Errorf("DeriveKey with empty password should return %d bytes, got %d", crypto.KeyLength, len(key))
}
}
// 1.5 Encrypt/Decrypt round-trip
func TestEncryptDecryptRoundTrip(t *testing.T) {
plaintext := []byte("hello world")
encoded, err := crypto.Encrypt(plaintext, "mypassword")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
decoded, err := crypto.Decrypt(encoded, "mypassword")
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
if !bytes.Equal(plaintext, decoded) {
t.Errorf("Round-trip failed: got %q, want %q", decoded, plaintext)
}
}
// 1.6 Encrypt empty plaintext
func TestEncryptDecryptEmpty(t *testing.T) {
plaintext := []byte("")
encoded, err := crypto.Encrypt(plaintext, "password")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
decoded, err := crypto.Decrypt(encoded, "password")
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
if len(decoded) != 0 {
t.Errorf("Expected empty plaintext, got %d bytes", len(decoded))
}
}
// 1.7 Encrypt large data (1MB)
func TestEncryptDecryptLargeData(t *testing.T) {
plaintext := make([]byte, 1024*1024)
if _, err := rand.Read(plaintext); err != nil {
t.Fatalf("Failed to generate random data: %v", err)
}
encoded, err := crypto.Encrypt(plaintext, "password")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
decoded, err := crypto.Decrypt(encoded, "password")
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
if !bytes.Equal(plaintext, decoded) {
t.Error("Large data round-trip failed")
}
}
// 1.8 Encrypt unicode
func TestEncryptDecryptUnicode(t *testing.T) {
plaintext := []byte("こんにちは世界 🌍")
encoded, err := crypto.Encrypt(plaintext, "password")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
decoded, err := crypto.Decrypt(encoded, "password")
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
if !bytes.Equal(plaintext, decoded) {
t.Errorf("Unicode round-trip failed: got %q, want %q", decoded, plaintext)
}
}
// 1.9 Encrypt with newlines
func TestEncryptDecryptNewlines(t *testing.T) {
plaintext := []byte("line1\nline2\nline3")
encoded, err := crypto.Encrypt(plaintext, "password")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
decoded, err := crypto.Decrypt(encoded, "password")
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
if !bytes.Equal(plaintext, decoded) {
t.Errorf("Newlines round-trip failed: got %q, want %q", decoded, plaintext)
}
}
// 1.10 Wrong password → error
func TestDecryptWrongPassword(t *testing.T) {
plaintext := []byte("secret data")
encoded, err := crypto.Encrypt(plaintext, "correct-password")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
_, err = crypto.Decrypt(encoded, "wrong-password")
if err == nil {
t.Error("Decrypt with wrong password should return error")
}
}
// 1.11 Empty password → error
func TestDecryptEmptyPassword(t *testing.T) {
plaintext := []byte("secret data")
encoded, err := crypto.Encrypt(plaintext, "password")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
_, err = crypto.Decrypt(encoded, "")
if err == nil {
t.Error("Decrypt with empty password should return error")
}
}
// 1.12 IsEncrypted valid ciphertext
func TestIsEncryptedValid(t *testing.T) {
encoded, err := crypto.Encrypt([]byte("test"), "password")
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
if !crypto.IsEncrypted(encoded) {
t.Error("IsEncrypted should return true for valid ciphertext")
}
}
// 1.13 IsEncrypted plaintext
func TestIsEncryptedPlaintext(t *testing.T) {
if crypto.IsEncrypted("hello world") {
t.Error("IsEncrypted should return false for plaintext")
}
}
// 1.14 IsEncrypted empty
func TestIsEncryptedEmpty(t *testing.T) {
if crypto.IsEncrypted("") {
t.Error("IsEncrypted should return false for empty string")
}
}
// 1.15 HashPassword determinism
func TestHashPasswordDeterminism(t *testing.T) {
hash1 := crypto.HashPassword("mypassword")
hash2 := crypto.HashPassword("mypassword")
if hash1 != hash2 {
t.Error("HashPassword should return same hash for same password")
}
}
// 1.16 HashPassword variation
func TestHashPasswordVariation(t *testing.T) {
hash1 := crypto.HashPassword("password1")
hash2 := crypto.HashPassword("password2")
if hash1 == hash2 {
t.Error("HashPassword should return different hashes for different passwords")
}
}
// 1.17 Encrypt randomness — same input → different ciphertext
func TestEncryptRandomness(t *testing.T) {
plaintext := []byte("same input")
encoded1, _ := crypto.Encrypt(plaintext, "password")
encoded2, _ := crypto.Encrypt(plaintext, "password")
if encoded1 == encoded2 {
t.Error("Encrypt should produce different ciphertext each time (random salt)")
}
}
// Verify constants
func TestConstants(t *testing.T) {
if crypto.KeyLength != 32 {
t.Errorf("KeyLength = %d, want 32", crypto.KeyLength)
}
if crypto.SaltLength != 16 {
t.Errorf("SaltLength = %d, want 16", crypto.SaltLength)
}
if crypto.Iterations != 100000 {
t.Errorf("Iterations = %d, want 100000", crypto.Iterations)
}
}
// Verify error variables
func TestErrorVars(t *testing.T) {
if crypto.ErrInvalidPassword == nil {
t.Error("ErrInvalidPassword should not be nil")
}
if crypto.ErrDecryptionFailed == nil {
t.Error("ErrDecryptionFailed should not be nil")
}
if !strings.Contains(crypto.ErrDecryptionFailed.Error(), "decryption failed") {
t.Error("ErrDecryptionFailed message should contain 'decryption failed'")
}
}