From 90f6e4ce07b41c74ee2ed1af542506b11354bfe7 Mon Sep 17 00:00:00 2001 From: Anand Date: Wed, 24 Sep 2025 17:04:35 +0530 Subject: [PATCH] More unit tests for {db,crypto,export} --- tests/crypto_test.go | 552 +++++++++++++++++++++++++++++++++++++++ tests/db_test.go | 571 +++++++++++++++++++++++++++++++++++++++++ tests/export_test.go | 595 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1718 insertions(+) create mode 100644 tests/crypto_test.go create mode 100644 tests/db_test.go create mode 100644 tests/export_test.go diff --git a/tests/crypto_test.go b/tests/crypto_test.go new file mode 100644 index 0000000..f1c4aa7 --- /dev/null +++ b/tests/crypto_test.go @@ -0,0 +1,552 @@ +package tests + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + "varuh" +) + +func TestGenerateRandomBytes(t *testing.T) { + tests := []struct { + name string + size int + wantErr bool + checkLen bool + }{ + {"valid small size", 16, false, true}, + {"valid medium size", 32, false, true}, + {"valid large size", 128, false, true}, + {"zero size", 0, false, true}, + {"negative size", -1, false, true}, // Go's make will panic on negative size + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.size < 0 { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic for negative size") + } + }() + } + + err, data := varuh.GenerateRandomBytes(tt.size) + + if (err != nil) != tt.wantErr { + t.Errorf("GenerateRandomBytes() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.checkLen && len(data) != tt.size { + t.Errorf("GenerateRandomBytes() returned data of length %d, want %d", len(data), tt.size) + } + + // Check that data is actually random (not all zeros) + if tt.size > 0 && !tt.wantErr { + allZeros := true + for _, b := range data { + if b != 0 { + allZeros = false + break + } + } + // Very unlikely to get all zeros with crypto/rand + if allZeros { + t.Error("GenerateRandomBytes() returned all zeros, likely not random") + } + } + }) + } +} + +func TestGenerateKeyArgon2(t *testing.T) { + tests := []struct { + name string + passPhrase string + salt *[]byte + wantErr bool + checkSalt bool + }{ + {"valid passphrase no salt", "test password", nil, false, true}, + {"valid passphrase with salt", "test password", func() *[]byte { s := make([]byte, varuh.SALT_SIZE); return &s }(), false, false}, + {"empty passphrase", "", nil, false, true}, + {"invalid salt size", "test", func() *[]byte { s := make([]byte, 10); return &s }(), true, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, key, salt := varuh.GenerateKeyArgon2(tt.passPhrase, tt.salt) + + if (err != nil) != tt.wantErr { + t.Errorf("GenerateKeyArgon2() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if len(key) != varuh.KEY_SIZE { + t.Errorf("GenerateKeyArgon2() returned key of length %d, want %d", len(key), varuh.KEY_SIZE) + } + + if tt.checkSalt && len(salt) != varuh.SALT_SIZE { + t.Errorf("GenerateKeyArgon2() returned salt of length %d, want %d", len(salt), varuh.SALT_SIZE) + } + + // Test deterministic key generation with same salt + if tt.salt != nil { + err2, key2, _ := varuh.GenerateKeyArgon2(tt.passPhrase, tt.salt) + if err2 != nil { + t.Errorf("Second GenerateKeyArgon2() call failed: %v", err2) + } else if !bytes.Equal(key, key2) { + t.Error("GenerateKeyArgon2() should produce same key with same salt") + } + } + } + }) + } +} + +func TestGenerateKey(t *testing.T) { + tests := []struct { + name string + passPhrase string + salt *[]byte + wantErr bool + checkSalt bool + }{ + {"valid passphrase no salt", "test password", nil, false, true}, + {"valid passphrase with salt", "test password", func() *[]byte { s := make([]byte, varuh.SALT_SIZE); return &s }(), false, false}, + {"empty passphrase", "", nil, false, true}, + {"invalid salt size", "test", func() *[]byte { s := make([]byte, 10); return &s }(), true, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, key, salt := varuh.GenerateKey(tt.passPhrase, tt.salt) + + if (err != nil) != tt.wantErr { + t.Errorf("GenerateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if len(key) != varuh.KEY_SIZE { + t.Errorf("GenerateKey() returned key of length %d, want %d", len(key), varuh.KEY_SIZE) + } + + if tt.checkSalt && len(salt) != varuh.SALT_SIZE { + t.Errorf("GenerateKey() returned salt of length %d, want %d", len(salt), varuh.SALT_SIZE) + } + + // Test deterministic key generation with same salt + if tt.salt != nil { + err2, key2, _ := varuh.GenerateKey(tt.passPhrase, tt.salt) + if err2 != nil { + t.Errorf("Second GenerateKey() call failed: %v", err2) + } else if !bytes.Equal(key, key2) { + t.Error("GenerateKey() should produce same key with same salt") + } + } + } + }) + } +} + +func TestIsFileEncrypted(t *testing.T) { + tempDir := t.TempDir() + + // Create test files + encryptedFile := filepath.Join(tempDir, "encrypted.db") + unencryptedFile := filepath.Join(tempDir, "unencrypted.db") + emptyFile := filepath.Join(tempDir, "empty.db") + nonExistentFile := filepath.Join(tempDir, "nonexistent.db") + + // Create encrypted file with magic header + magicBytes := []byte(fmt.Sprintf("%x", varuh.MAGIC_HEADER)) + err := os.WriteFile(encryptedFile, append(magicBytes, []byte("some encrypted data")...), 0644) + if err != nil { + t.Fatalf("Failed to create test encrypted file: %v", err) + } + + // Create unencrypted file + err = os.WriteFile(unencryptedFile, []byte("regular data"), 0644) + if err != nil { + t.Fatalf("Failed to create test unencrypted file: %v", err) + } + + // Create empty file + err = os.WriteFile(emptyFile, []byte{}, 0644) + if err != nil { + t.Fatalf("Failed to create test empty file: %v", err) + } + + tests := []struct { + name string + filePath string + wantErr bool + encrypted bool + }{ + {"encrypted file", encryptedFile, false, true}, + {"unencrypted file", unencryptedFile, true, false}, + {"empty file", emptyFile, true, false}, + {"non-existent file", nonExistentFile, true, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, encrypted := varuh.IsFileEncrypted(tt.filePath) + + if (err != nil) != tt.wantErr { + t.Errorf("IsFileEncrypted() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if encrypted != tt.encrypted { + t.Errorf("IsFileEncrypted() encrypted = %v, want %v", encrypted, tt.encrypted) + } + }) + } +} + +func TestEncryptFileAES(t *testing.T) { + tempDir := t.TempDir() + testFile := filepath.Join(tempDir, "test.db") + testContent := []byte("This is test database content for AES encryption") + + err := os.WriteFile(testFile, testContent, 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + tests := []struct { + name string + dbPath string + password string + wantErr bool + }{ + {"valid encryption", testFile, "testpassword", false}, + {"empty password", testFile, "", false}, // Empty password should still work + {"non-existent file", filepath.Join(tempDir, "nonexistent.db"), "password", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.EncryptFileAES(tt.dbPath, tt.password) + + if (err != nil) != tt.wantErr { + t.Errorf("EncryptFileAES() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + // Verify the file is now encrypted + err, encrypted := varuh.IsFileEncrypted(tt.dbPath) + if err != nil { + t.Errorf("Failed to check if file is encrypted: %v", err) + } else if !encrypted { + t.Error("File should be encrypted after EncryptFileAES()") + } + } + }) + } +} + +func TestDecryptFileAES(t *testing.T) { + tempDir := t.TempDir() + testFile := filepath.Join(tempDir, "test.db") + testContent := []byte("This is test database content for AES decryption") + password := "testpassword" + + // Create and encrypt a test file + err := os.WriteFile(testFile, testContent, 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + err = varuh.EncryptFileAES(testFile, password) + if err != nil { + t.Fatalf("Failed to encrypt test file: %v", err) + } + + tests := []struct { + name string + filePath string + password string + wantErr bool + }{ + {"valid decryption", testFile, password, false}, + {"wrong password", testFile, "wrongpassword", true}, + {"non-existent file", filepath.Join(tempDir, "nonexistent.db"), password, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Re-encrypt file for each test case + if tt.name != "non-existent file" { + os.WriteFile(testFile, testContent, 0644) + varuh.EncryptFileAES(testFile, password) + } + + err := varuh.DecryptFileAES(tt.filePath, tt.password) + + if (err != nil) != tt.wantErr { + t.Errorf("DecryptFileAES() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + // Verify the file is decrypted and content is correct + decryptedContent, err := os.ReadFile(tt.filePath) + if err != nil { + t.Errorf("Failed to read decrypted file: %v", err) + } else if !bytes.Equal(decryptedContent, testContent) { + t.Error("Decrypted content doesn't match original content") + } + } + }) + } +} + +func TestEncryptFileXChachaPoly(t *testing.T) { + tempDir := t.TempDir() + testFile := filepath.Join(tempDir, "test.db") + testContent := []byte("This is test database content for XChaCha encryption") + + err := os.WriteFile(testFile, testContent, 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + tests := []struct { + name string + dbPath string + password string + wantErr bool + }{ + {"valid encryption", testFile, "testpassword", false}, + {"empty password", testFile, "", false}, + {"non-existent file", filepath.Join(tempDir, "nonexistent.db"), "password", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.EncryptFileXChachaPoly(tt.dbPath, tt.password) + + if (err != nil) != tt.wantErr { + t.Errorf("EncryptFileXChachaPoly() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + // Verify the file is now encrypted + err, encrypted := varuh.IsFileEncrypted(tt.dbPath) + if err != nil { + t.Errorf("Failed to check if file is encrypted: %v", err) + } else if !encrypted { + t.Error("File should be encrypted after EncryptFileXChachaPoly()") + } + } + }) + } +} + +func TestDecryptFileXChachaPoly(t *testing.T) { + tempDir := t.TempDir() + testFile := filepath.Join(tempDir, "test.db") + testContent := []byte("This is test database content for XChaCha decryption") + password := "testpassword" + + // Create and encrypt a test file + err := os.WriteFile(testFile, testContent, 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + err = varuh.EncryptFileXChachaPoly(testFile, password) + if err != nil { + t.Fatalf("Failed to encrypt test file: %v", err) + } + + tests := []struct { + name string + filePath string + password string + wantErr bool + }{ + {"valid decryption", testFile, password, false}, + {"wrong password", testFile, "wrongpassword", true}, + {"non-existent file", filepath.Join(tempDir, "nonexistent.db"), password, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Re-encrypt file for each test case + if tt.name != "non-existent file" { + os.WriteFile(testFile, testContent, 0644) + varuh.EncryptFileXChachaPoly(testFile, password) + } + + err := varuh.DecryptFileXChachaPoly(tt.filePath, tt.password) + + if (err != nil) != tt.wantErr { + t.Errorf("DecryptFileXChachaPoly() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + // Verify the file is decrypted and content is correct + decryptedContent, err := os.ReadFile(tt.filePath) + if err != nil { + t.Errorf("Failed to read decrypted file: %v", err) + } else if !bytes.Equal(decryptedContent, testContent) { + t.Error("Decrypted content doesn't match original content") + } + } + }) + } +} + +func TestGeneratePassword(t *testing.T) { + tests := []struct { + name string + length int + wantErr bool + }{ + {"zero length", 0, false}, + {"small length", 8, false}, + {"medium length", 16, false}, + {"large length", 64, false}, + {"negative length", -1, false}, // This should handle gracefully or error + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.length < 0 { + // Expected to either handle gracefully or panic + defer func() { + recover() // Catch any panic + }() + } + + err, password := varuh.GeneratePassword(tt.length) + + if (err != nil) != tt.wantErr { + t.Errorf("GeneratePassword() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.length >= 0 { + if len(password) != tt.length { + t.Errorf("GeneratePassword() returned password of length %d, want %d", len(password), tt.length) + } + + // Check that password contains only valid characters + const validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=+_()$#@!~:/%" + for _, char := range password { + found := false + for _, validChar := range validChars { + if char == validChar { + found = true + break + } + } + if !found { + t.Errorf("GeneratePassword() returned invalid character: %c", char) + } + } + } + }) + } +} + +func TestGenerateStrongPassword(t *testing.T) { + // Run multiple times since it's random + for i := 0; i < 10; i++ { + t.Run(fmt.Sprintf("iteration_%d", i+1), func(t *testing.T) { + err, password := varuh.GenerateStrongPassword() + + if err != nil { + t.Errorf("GenerateStrongPassword() error = %v", err) + return + } + + // Check minimum length + if len(password) < 12 { + t.Errorf("GenerateStrongPassword() returned password of length %d, minimum expected 12", len(password)) + } + + // Check maximum expected length (should be 16 or less based on implementation) + if len(password) > 16 { + t.Errorf("GenerateStrongPassword() returned password of length %d, maximum expected 16", len(password)) + } + + // Check that it contains various character types + hasLower := false + hasUpper := false + hasDigit := false + hasPunct := false + + const lowerChars = "abcdefghijklmnopqrstuvwxyz" + const upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + const digitChars = "0123456789" + const punctChars = "=+_()$#@!~:/%" + + for _, char := range password { + switch { + case containsChar(lowerChars, char): + hasLower = true + case containsChar(upperChars, char): + hasUpper = true + case containsChar(digitChars, char): + hasDigit = true + case containsChar(punctChars, char): + hasPunct = true + } + } + + if !hasLower { + t.Error("GenerateStrongPassword() should contain lowercase characters") + } + if !hasUpper { + t.Error("GenerateStrongPassword() should contain uppercase characters") + } + if !hasDigit { + t.Error("GenerateStrongPassword() should contain digit characters") + } + if !hasPunct { + t.Error("GenerateStrongPassword() should contain punctuation characters") + } + }) + } +} + +// Helper function to check if a string contains a character +func containsChar(s string, char rune) bool { + for _, c := range s { + if c == char { + return true + } + } + return false +} + +// Benchmark tests +func BenchmarkGenerateRandomBytes(b *testing.B) { + for i := 0; i < b.N; i++ { + varuh.GenerateRandomBytes(32) + } +} + +func BenchmarkGenerateKeyArgon2(b *testing.B) { + for i := 0; i < b.N; i++ { + varuh.GenerateKeyArgon2("test password", nil) + } +} + +func BenchmarkGenerateKey(b *testing.B) { + for i := 0; i < b.N; i++ { + varuh.GenerateKey("test password", nil) + } +} diff --git a/tests/db_test.go b/tests/db_test.go new file mode 100644 index 0000000..5c47abf --- /dev/null +++ b/tests/db_test.go @@ -0,0 +1,571 @@ +package tests + +import ( + "log" + "path/filepath" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "testing" + "varuh" +) + +func createMockDb(fileName string) error { + // Just open it with GORM/SQLite driver + db, err := gorm.Open(sqlite.Open(fileName), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + return err + } + + // This will create the DB file with a proper SQLite header if it doesnt exist + sqlDB, _ := db.DB() + defer sqlDB.Close() + + log.Printf("SQLite database %s created and ready.\n", fileName) + + return nil +} + + +func TestEntry_Copy(t *testing.T) { + tests := []struct { + name string + e1 *varuh.Entry + e2 *varuh.Entry + want *varuh.Entry + }{ + { + name: "copy password entry", + e1: &varuh.Entry{}, + e2: &varuh.Entry{ + Title: "Test Title", + User: "test@example.com", + Url: "https://example.com", + Password: "secret123", + Notes: "Test notes", + Tags: "test,example", + Type: "password", + }, + want: &varuh.Entry{ + Title: "Test Title", + User: "test@example.com", + Url: "https://example.com", + Password: "secret123", + Notes: "Test notes", + Tags: "test,example", + Type: "password", + }, + }, + { + name: "copy card entry", + e1: &varuh.Entry{}, + e2: &varuh.Entry{ + Title: "Test Card", + User: "John Doe", + Issuer: "Chase Bank", + Url: "4111111111111111", + Password: "123", + ExpiryDate: "12/25", + Tags: "credit,card", + Notes: "Main card", + Type: "card", + }, + want: &varuh.Entry{ + Title: "Test Card", + User: "John Doe", + Issuer: "Chase Bank", + Url: "4111111111111111", + Password: "123", + ExpiryDate: "12/25", + Tags: "credit,card", + Notes: "Main card", + Type: "card", + }, + }, + { + name: "copy nil entry", + e1: &varuh.Entry{Title: "Original", User: "original@test.com"}, + e2: nil, + want: &varuh.Entry{Title: "Original", User: "original@test.com"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.e1.Copy(tt.e2) + + if tt.e2 == nil { + // Should remain unchanged + if tt.e1.Title != tt.want.Title || tt.e1.User != tt.want.User { + t.Errorf("Entry should remain unchanged when copying nil") + } + return + } + + // Compare relevant fields based on type + switch tt.e2.Type { + case "password": + if tt.e1.Title != tt.want.Title || + tt.e1.User != tt.want.User || + tt.e1.Url != tt.want.Url || + tt.e1.Password != tt.want.Password || + tt.e1.Notes != tt.want.Notes || + tt.e1.Tags != tt.want.Tags || + tt.e1.Type != tt.want.Type { + t.Errorf("Password entry copy failed") + } + case "card": + if tt.e1.Title != tt.want.Title || + tt.e1.User != tt.want.User || + tt.e1.Issuer != tt.want.Issuer || + tt.e1.Url != tt.want.Url || + tt.e1.Password != tt.want.Password || + tt.e1.ExpiryDate != tt.want.ExpiryDate || + tt.e1.Tags != tt.want.Tags || + tt.e1.Notes != tt.want.Notes || + tt.e1.Type != tt.want.Type { + t.Errorf("Card entry copy failed") + } + } + }) + } +} + +func TestExtendedEntry_Copy(t *testing.T) { + tests := []struct { + name string + e1 *varuh.ExtendedEntry + e2 *varuh.ExtendedEntry + want *varuh.ExtendedEntry + }{ + { + name: "copy extended entry", + e1: &varuh.ExtendedEntry{}, + e2: &varuh.ExtendedEntry{ + FieldName: "CustomField1", + FieldValue: "CustomValue1", + EntryID: 123, + }, + want: &varuh.ExtendedEntry{ + FieldName: "CustomField1", + FieldValue: "CustomValue1", + EntryID: 123, + }, + }, + { + name: "copy nil extended entry", + e1: &varuh.ExtendedEntry{ + FieldName: "Original", + FieldValue: "OriginalValue", + EntryID: 1, + }, + e2: nil, + want: &varuh.ExtendedEntry{ + FieldName: "Original", + FieldValue: "OriginalValue", + EntryID: 1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.e1.Copy(tt.e2) + + if tt.e2 == nil { + // Should remain unchanged + if tt.e1.FieldName != tt.want.FieldName || + tt.e1.FieldValue != tt.want.FieldValue || + tt.e1.EntryID != tt.want.EntryID { + t.Errorf("ExtendedEntry should remain unchanged when copying nil") + } + return + } + + if tt.e1.FieldName != tt.want.FieldName || + tt.e1.FieldValue != tt.want.FieldValue || + tt.e1.EntryID != tt.want.EntryID { + t.Errorf("ExtendedEntry copy failed, got FieldName=%s FieldValue=%s EntryID=%d, want FieldName=%s FieldValue=%s EntryID=%d", + tt.e1.FieldName, tt.e1.FieldValue, tt.e1.EntryID, + tt.want.FieldName, tt.want.FieldValue, tt.want.EntryID) + } + }) + } +} + +func TestOpenDatabase(t *testing.T) { + tempDir := t.TempDir() + + // Create a test SQLite database file + testDB := filepath.Join(tempDir, "test.db") + err := createMockDb(testDB) + if err != nil { + t.Fatalf("Failed to create test database file: %v", err) + } + + tests := []struct { + name string + filePath string + wantErr bool + }{ + {"valid database", testDB, false}, + {"empty path", "", true}, + {"non-existent file", filepath.Join(tempDir, "nonexistent.db"), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, db := varuh.OpenDatabase(tt.filePath) + + if (err != nil) != tt.wantErr { + t.Errorf("OpenDatabase() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && db == nil { + t.Error("OpenDatabase() returned nil database for valid input") + } + }) + } +} + +func TestCreateNewEntry(t *testing.T) { + tempDir := t.TempDir() + testDB := filepath.Join(tempDir, "test.db") + + // Create a basic SQLite file + err := createMockDb(testDB) + if err != nil { + t.Fatalf("Failed to create test database file: %v", err) + } + + err, db := varuh.OpenDatabase(testDB) + if err != nil { + t.Fatalf("Failed to open test database: %v", err) + } + + err = varuh.CreateNewEntry(db) + if err != nil { + t.Errorf("CreateNewEntry() error = %v", err) + } +} + +func TestCreateNewExEntry(t *testing.T) { + tempDir := t.TempDir() + testDB := filepath.Join(tempDir, "test.db") + + // Create a basic SQLite file + err := createMockDb(testDB) + if err != nil { + t.Fatalf("Failed to create test database file: %v", err) + } + + err, db := varuh.OpenDatabase(testDB) + if err != nil { + t.Fatalf("Failed to open test database: %v", err) + } + + err = varuh.CreateNewExEntry(db) + if err != nil { + t.Errorf("CreateNewExEntry() error = %v", err) + } +} + +func TestInitNewDatabase(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + dbPath string + wantErr bool + }{ + {"valid path", filepath.Join(tempDir, "new.db"), false}, + {"empty path", "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Skip tests that depend on global state/config + if tt.name == "valid path" { + t.Skip("Skipping InitNewDatabase test as it depends on global config state") + } + + err := varuh.InitNewDatabase(tt.dbPath) + + if (err != nil) != tt.wantErr { + t.Errorf("InitNewDatabase() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetEntryById(t *testing.T) { + // This function depends on active database state + // We'll test that it doesn't panic and returns appropriate error + t.Run("no active database", func(t *testing.T) { + err, entry := varuh.GetEntryById(1) + + // Should return an error or nil entry when no active database + if err == nil && entry != nil { + t.Error("GetEntryById() should return error or nil entry when no active database") + } + }) + + t.Run("invalid id", func(t *testing.T) { + err, entry := varuh.GetEntryById(-1) + + // Should handle invalid IDs gracefully + if entry != nil && entry.ID == -1 { + t.Error("GetEntryById() should not return entry with invalid ID") + } + _ = err // err can be nil or non-nil depending on database state + }) +} + +func TestSearchDatabaseEntry(t *testing.T) { + // This function depends on active database state + t.Run("empty search term", func(t *testing.T) { + err, entries := varuh.SearchDatabaseEntry("") + + // Should handle empty search term gracefully + _ = err // err can be nil or non-nil depending on database state + _ = entries // entries can be empty or nil + }) + + t.Run("normal search term", func(t *testing.T) { + err, entries := varuh.SearchDatabaseEntry("test") + + // Should handle normal search without panic + _ = err // err can be nil or non-nil depending on database state + _ = entries // entries can be empty or nil + }) +} + +func TestUnion(t *testing.T) { + entry1 := varuh.Entry{ID: 1, Title: "Entry 1"} + entry2 := varuh.Entry{ID: 2, Title: "Entry 2"} + entry3 := varuh.Entry{ID: 3, Title: "Entry 3"} + entry1Dup := varuh.Entry{ID: 1, Title: "Entry 1 Duplicate"} + + tests := []struct { + name string + slice1 []varuh.Entry + slice2 []varuh.Entry + want int // expected length of result + }{ + { + name: "empty slices", + slice1: []varuh.Entry{}, + slice2: []varuh.Entry{}, + want: 0, + }, + { + name: "no overlap", + slice1: []varuh.Entry{entry1, entry2}, + slice2: []varuh.Entry{entry3}, + want: 3, + }, + { + name: "with overlap", + slice1: []varuh.Entry{entry1, entry2}, + slice2: []varuh.Entry{entry1Dup, entry3}, + want: 3, // should not duplicate entry1 + }, + } + + // Use reflection to call the unexported union function + // Since it's unexported, we'll test the public functions that use it + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We can't directly test the unexported union function + // So we test SearchDatabaseEntries which uses it internally + terms := []string{"term1", "term2"} + _, entries := varuh.SearchDatabaseEntries(terms, "OR") + + // Just ensure no panic occurs and entries is a valid slice + if entries == nil { + entries = []varuh.Entry{} + } + _ = len(entries) // Use the result to avoid unused variable error + }) + } +} + +func TestSearchDatabaseEntries(t *testing.T) { + tests := []struct { + name string + terms []string + operator string + }{ + {"empty terms", []string{}, "AND"}, + {"single term", []string{"test"}, "AND"}, + {"multiple terms AND", []string{"test", "example"}, "AND"}, + {"multiple terms OR", []string{"test", "example"}, "OR"}, + {"invalid operator", []string{"test"}, "INVALID"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, entries := varuh.SearchDatabaseEntries(tt.terms, tt.operator) + + // Should handle all cases without panic + _ = err // err can be nil or non-nil depending on database state + _ = entries // entries can be empty or nil + }) + } +} + +func TestRemoveDatabaseEntry(t *testing.T) { + entry := &varuh.Entry{ID: 1, Title: "Test Entry"} + + t.Run("remove entry", func(t *testing.T) { + err := varuh.RemoveDatabaseEntry(entry) + + // Should handle gracefully whether or not there's an active database + _ = err // err can be nil or non-nil depending on database state + }) +} + +func TestCloneEntry(t *testing.T) { + entry := &varuh.Entry{ + ID: 1, + Title: "Original Entry", + User: "user@example.com", + Password: "secret123", + Type: "password", + } + + t.Run("clone entry", func(t *testing.T) { + err, clonedEntry := varuh.CloneEntry(entry) + + // Should handle gracefully whether or not there's an active database + _ = err // err can be nil or non-nil depending on database state + _ = clonedEntry // clonedEntry can be nil if no active database + }) +} + +func TestIterateEntries(t *testing.T) { + tests := []struct { + name string + orderKey string + order string + }{ + {"order by id asc", "id", "asc"}, + {"order by title desc", "title", "desc"}, + {"order by timestamp asc", "timestamp", "asc"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, entries := varuh.IterateEntries(tt.orderKey, tt.order) + + // Should handle all cases without panic + _ = err // err can be nil or non-nil depending on database state + _ = entries // entries can be empty or nil + }) + } +} + +func TestEntriesToStringArray(t *testing.T) { + tests := []struct { + name string + skipLongFields bool + }{ + {"include long fields", false}, + {"skip long fields", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, dataArray := varuh.EntriesToStringArray(tt.skipLongFields) + + // Should handle gracefully whether or not there's an active database + _ = err // err can be nil or non-nil depending on database state + _ = dataArray // dataArray can be empty or nil + }) + } +} + +func TestGetExtendedEntries(t *testing.T) { + entry := &varuh.Entry{ID: 1, Title: "Test Entry"} + + t.Run("get extended entries", func(t *testing.T) { + extEntries := varuh.GetExtendedEntries(entry) + + // Should return a valid slice (can be empty) + if extEntries == nil { + extEntries = []varuh.ExtendedEntry{} + } + _ = len(extEntries) // Use the result + }) +} + +// Integration tests that would work with actual database setup +func TestEntryTableName(t *testing.T) { + entry := &varuh.Entry{} + if entry.TableName() != "entries" { + t.Errorf("Entry.TableName() = %s, want entries", entry.TableName()) + } +} + +func TestExtendedEntryTableName(t *testing.T) { + extEntry := &varuh.ExtendedEntry{} + if extEntry.TableName() != "exentries" { + t.Errorf("ExtendedEntry.TableName() = %s, want exentries", extEntry.TableName()) + } +} + +func TestAddressTableName(t *testing.T) { + address := &varuh.Address{} + if address.TableName() != "address" { + t.Errorf("Address.TableName() = %s, want address", address.TableName()) + } +} + +// Test that database operations handle nil inputs gracefully +func TestDatabaseOperationsWithNilInputs(t *testing.T) { + t.Run("operations with nil entry", func(t *testing.T) { + // Test that functions handle nil entries gracefully + err := varuh.RemoveDatabaseEntry(nil) + _ = err // Should not panic, may return error + + _, cloned := varuh.CloneEntry(nil) + _ = cloned // Should not panic, may return nil + + extEntries := varuh.GetExtendedEntries(nil) + if extEntries == nil { + extEntries = []varuh.ExtendedEntry{} + } + _ = len(extEntries) + }) +} + +// Benchmark tests +func BenchmarkEntry_Copy(b *testing.B) { + e1 := &varuh.Entry{} + e2 := &varuh.Entry{ + Title: "Benchmark Title", + User: "bench@example.com", + Password: "secret123", + Type: "password", + } + + for i := 0; i < b.N; i++ { + e1.Copy(e2) + } +} + +func BenchmarkExtendedEntry_Copy(b *testing.B) { + e1 := &varuh.ExtendedEntry{} + e2 := &varuh.ExtendedEntry{ + FieldName: "BenchField", + FieldValue: "BenchValue", + EntryID: 1, + } + + for i := 0; i < b.N; i++ { + e1.Copy(e2) + } +} diff --git a/tests/export_test.go b/tests/export_test.go new file mode 100644 index 0000000..1c95383 --- /dev/null +++ b/tests/export_test.go @@ -0,0 +1,595 @@ +package tests + +import ( + "encoding/csv" + "os" + "path/filepath" + "strings" + "testing" + "varuh" +) + +func TestExportToFile(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + fileName string + wantErr bool + errMsg string + }{ + { + name: "unsupported extension", + fileName: filepath.Join(tempDir, "test.txt"), + wantErr: true, + errMsg: "format .txt not supported", + }, + { + name: "csv extension", + fileName: filepath.Join(tempDir, "test.csv"), + wantErr: false, // May error due to no active database, but format is supported + }, + { + name: "markdown extension", + fileName: filepath.Join(tempDir, "test.md"), + wantErr: false, // May error due to no active database, but format is supported + }, + { + name: "html extension", + fileName: filepath.Join(tempDir, "test.html"), + wantErr: false, // May error due to no active database, but format is supported + }, + { + name: "pdf extension", + fileName: filepath.Join(tempDir, "test.pdf"), + wantErr: false, // May error due to dependencies, but format is supported + }, + { + name: "uppercase extension", + fileName: filepath.Join(tempDir, "test.CSV"), + wantErr: false, // Should handle case-insensitive extensions + }, + { + name: "no extension", + fileName: filepath.Join(tempDir, "test"), + wantErr: true, + errMsg: "format not supported", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.ExportToFile(tt.fileName) + + if tt.wantErr && (err == nil || !strings.Contains(err.Error(), tt.errMsg)) { + if tt.errMsg != "" { + t.Errorf("ExportToFile() expected error containing '%s', got %v", tt.errMsg, err) + } else { + t.Errorf("ExportToFile() expected error, got nil") + } + return + } + + // For supported formats, we expect either success or database-related errors + if !tt.wantErr && err != nil { + // It's okay to get database-related errors when no active database is configured + validErrors := []string{ + "database path cannot be empty", + "Error exporting entries", + "Error opening active database", + "pandoc not found", + } + + hasValidError := false + for _, validErr := range validErrors { + if strings.Contains(err.Error(), validErr) { + hasValidError = true + break + } + } + + if !hasValidError { + t.Errorf("ExportToFile() unexpected error for supported format: %v", err) + } + } + }) + } +} + +func TestExportToCSV(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + fileName string + wantErr bool + }{ + { + name: "valid csv file", + fileName: filepath.Join(tempDir, "export.csv"), + wantErr: false, // May error due to database, but CSV writing logic should work + }, + { + name: "invalid directory", + fileName: "/nonexistent/directory/export.csv", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.ExportToCSV(tt.fileName) + + if tt.wantErr { + if err == nil { + t.Errorf("ExportToCSV() expected error, got nil") + } + return + } + + // For valid filenames, we expect either success or database-related errors + if err != nil { + validErrors := []string{ + "database path cannot be empty", + "Error exporting entries", + "Error opening active database", + } + + hasValidError := false + for _, validErr := range validErrors { + if strings.Contains(err.Error(), validErr) { + hasValidError = true + break + } + } + + if !hasValidError { + t.Errorf("ExportToCSV() unexpected error: %v", err) + } + } + }) + } +} + +func TestExportToMarkdown(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + fileName string + wantErr bool + }{ + { + name: "valid markdown file", + fileName: filepath.Join(tempDir, "export.md"), + wantErr: false, + }, + { + name: "invalid directory", + fileName: "/nonexistent/directory/export.md", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.ExportToMarkdown(tt.fileName) + + if tt.wantErr { + if err == nil { + t.Errorf("ExportToMarkdown() expected error, got nil") + } + return + } + + // For valid filenames, we expect either success or database-related errors + if err != nil { + validErrors := []string{ + "database path cannot be empty", + "Error exporting entries", + "Error opening active database", + } + + hasValidError := false + for _, validErr := range validErrors { + if strings.Contains(err.Error(), validErr) { + hasValidError = true + break + } + } + + if !hasValidError { + t.Errorf("ExportToMarkdown() unexpected error: %v", err) + } + } + }) + } +} + +func TestExportToMarkdownLimited(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + fileName string + wantErr bool + }{ + { + name: "valid markdown file", + fileName: filepath.Join(tempDir, "export_limited.md"), + wantErr: false, + }, + { + name: "invalid directory", + fileName: "/nonexistent/directory/export_limited.md", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.ExportToMarkdownLimited(tt.fileName) + + if tt.wantErr { + if err == nil { + t.Errorf("ExportToMarkdownLimited() expected error, got nil") + } + return + } + + // For valid filenames, we expect either success or database-related errors + if err != nil { + validErrors := []string{ + "database path cannot be empty", + "Error exporting entries", + "Error opening active database", + } + + hasValidError := false + for _, validErr := range validErrors { + if strings.Contains(err.Error(), validErr) { + hasValidError = true + break + } + } + + if !hasValidError { + t.Errorf("ExportToMarkdownLimited() unexpected error: %v", err) + } + } + }) + } +} + +func TestExportToHTML(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + fileName string + wantErr bool + }{ + { + name: "valid html file", + fileName: filepath.Join(tempDir, "export.html"), + wantErr: false, + }, + { + name: "invalid directory", + fileName: "/nonexistent/directory/export.html", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.ExportToHTML(tt.fileName) + + if tt.wantErr { + if err == nil { + t.Errorf("ExportToHTML() expected error, got nil") + } + return + } + + // For valid filenames, we expect either success or database-related errors + if err != nil { + validErrors := []string{ + "database path cannot be empty", + "Error exporting entries", + "Error opening active database", + } + + hasValidError := false + for _, validErr := range validErrors { + if strings.Contains(err.Error(), validErr) { + hasValidError = true + break + } + } + + if !hasValidError { + t.Errorf("ExportToHTML() unexpected error: %v", err) + } + } + }) + } +} + +func TestExportToPDF(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + fileName string + wantErr bool + }{ + { + name: "valid pdf file", + fileName: filepath.Join(tempDir, "export.pdf"), + wantErr: false, // May error due to pandoc dependency, but that's expected + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := varuh.ExportToPDF(tt.fileName) + + // PDF export requires external dependencies (pandoc, pdftk) + // So we expect it to fail in most test environments + // We mainly test that it doesn't panic and handles errors gracefully + if err != nil { + validErrors := []string{ + "pandoc not found", + "database path cannot be empty", + "Error exporting entries", + "Error opening active database", + } + + hasValidError := false + for _, validErr := range validErrors { + if strings.Contains(err.Error(), validErr) { + hasValidError = true + break + } + } + + if !hasValidError { + t.Logf("ExportToPDF() returned unexpected error (may be system-specific): %v", err) + } + } + }) + } +} + +// Test export functions with mock data +func TestExportWithMockData(t *testing.T) { + tempDir := t.TempDir() + + // Create a mock CSV file to test the CSV export format structure + t.Run("mock csv structure", func(t *testing.T) { + csvFile := filepath.Join(tempDir, "mock_test.csv") + + // Create a simple CSV file manually to test structure + fh, err := os.Create(csvFile) + if err != nil { + t.Fatalf("Failed to create mock CSV file: %v", err) + } + + writer := csv.NewWriter(fh) + + // Write header (same as in ExportToCSV) + header := []string{"ID", "Title", "User", "URL", "Password", "Notes", "Modified"} + err = writer.Write(header) + if err != nil { + t.Fatalf("Failed to write CSV header: %v", err) + } + + // Write a mock record + record := []string{"1", "Test Entry", "user@example.com", "https://example.com", "secret123", "Test notes", "2023-01-01 12:00:00"} + err = writer.Write(record) + if err != nil { + t.Fatalf("Failed to write CSV record: %v", err) + } + + writer.Flush() + fh.Close() + + // Verify the file was created and has expected structure + if _, err := os.Stat(csvFile); os.IsNotExist(err) { + t.Error("Mock CSV file was not created") + } + + // Read back and verify + content, err := os.ReadFile(csvFile) + if err != nil { + t.Fatalf("Failed to read mock CSV file: %v", err) + } + + contentStr := string(content) + if !strings.Contains(contentStr, "ID,Title,User") { + t.Error("CSV header not found in mock file") + } + + if !strings.Contains(contentStr, "Test Entry") { + t.Error("Test record not found in mock CSV file") + } + }) + + // Test markdown structure + t.Run("mock markdown structure", func(t *testing.T) { + mdFile := filepath.Join(tempDir, "mock_test.md") + + // Create a simple markdown file manually to test structure + fh, err := os.Create(mdFile) + if err != nil { + t.Fatalf("Failed to create mock markdown file: %v", err) + } + defer fh.Close() + + // Write markdown table (similar to ExportToMarkdown) + content := ` | ID | Title | User | URL | Password | Notes | Modified | + | --- | --- | --- | --- | --- | --- | --- | + | 1 | Test Entry | user@example.com | https://example.com | secret123 | Test notes | 2023-01-01 12:00:00 | +` + _, err = fh.WriteString(content) + if err != nil { + t.Fatalf("Failed to write markdown content: %v", err) + } + + // Verify the file was created + if _, err := os.Stat(mdFile); os.IsNotExist(err) { + t.Error("Mock markdown file was not created") + } + + // Read back and verify structure + readContent, err := os.ReadFile(mdFile) + if err != nil { + t.Fatalf("Failed to read mock markdown file: %v", err) + } + + contentStr := string(readContent) + if !strings.Contains(contentStr, "| ID | Title |") { + t.Error("Markdown table header not found") + } + + if !strings.Contains(contentStr, "| --- |") { + t.Error("Markdown table separator not found") + } + }) + + // Test HTML structure + t.Run("mock html structure", func(t *testing.T) { + htmlFile := filepath.Join(tempDir, "mock_test.html") + + // Create a simple HTML file manually to test structure + fh, err := os.Create(htmlFile) + if err != nil { + t.Fatalf("Failed to create mock HTML file: %v", err) + } + defer fh.Close() + + // Write HTML table (similar to ExportToHTML) + content := ` + + + + + + +
ID Title User URL Password Notes Modified
1Test Entryuser@example.comhttps://example.comsecret123Test notes2023-01-01 12:00:00
+ +` + _, err = fh.WriteString(content) + if err != nil { + t.Fatalf("Failed to write HTML content: %v", err) + } + + // Verify the file was created + if _, err := os.Stat(htmlFile); os.IsNotExist(err) { + t.Error("Mock HTML file was not created") + } + + // Read back and verify structure + readContent, err := os.ReadFile(htmlFile) + if err != nil { + t.Fatalf("Failed to read mock HTML file: %v", err) + } + + contentStr := string(readContent) + if !strings.Contains(contentStr, " ID ") { + t.Error("HTML table header not found") + } + + if !strings.Contains(contentStr, "Test Entry") { + t.Error("HTML table data not found") + } + }) +} + +// Test edge cases +func TestExportEdgeCases(t *testing.T) { + tempDir := t.TempDir() + + t.Run("empty filename", func(t *testing.T) { + err := varuh.ExportToFile("") + if err == nil || !strings.Contains(err.Error(), "format not supported") { + t.Errorf("ExportToFile() with empty filename should return unsupported format error, got: %v", err) + } + }) + + t.Run("filename with dots", func(t *testing.T) { + filename := filepath.Join(tempDir, "test.file.csv") + err := varuh.ExportToFile(filename) + // Should handle filename with multiple dots correctly (use last extension) + if err != nil { + validErrors := []string{ + "database path cannot be empty", + "Error exporting entries", + "Error opening active database", + } + + hasValidError := false + for _, validErr := range validErrors { + if strings.Contains(err.Error(), validErr) { + hasValidError = true + break + } + } + + if !hasValidError { + t.Errorf("ExportToFile() with dotted filename unexpected error: %v", err) + } + } + }) + + t.Run("readonly directory", func(t *testing.T) { + readonlyDir := filepath.Join(tempDir, "readonly") + err := os.Mkdir(readonlyDir, 0400) // Read-only directory + if err != nil { + t.Skipf("Cannot create readonly directory: %v", err) + } + defer os.Chmod(readonlyDir, 0755) // Restore permissions for cleanup + + filename := filepath.Join(readonlyDir, "export.csv") + err = varuh.ExportToCSV(filename) + if err == nil { + t.Error("ExportToCSV() should fail when writing to readonly directory") + } + }) +} + +// Benchmark tests +func BenchmarkExportToFile(b *testing.B) { + tempDir := b.TempDir() + + for i := 0; i < b.N; i++ { + filename := filepath.Join(tempDir, "benchmark_test.csv") + varuh.ExportToFile(filename) + os.Remove(filename) // Clean up for next iteration + } +} + +func BenchmarkExportToCSV(b *testing.B) { + tempDir := b.TempDir() + + for i := 0; i < b.N; i++ { + filename := filepath.Join(tempDir, "benchmark_test.csv") + varuh.ExportToCSV(filename) + os.Remove(filename) // Clean up for next iteration + } +} + +func BenchmarkExportToMarkdown(b *testing.B) { + tempDir := b.TempDir() + + for i := 0; i < b.N; i++ { + filename := filepath.Join(tempDir, "benchmark_test.md") + varuh.ExportToMarkdown(filename) + os.Remove(filename) // Clean up for next iteration + } +} \ No newline at end of file