Files
swanadiva 847989df75 refactor: move V1 code into v1/ subdirectory
- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/
- Create v1/README.md with V1 documentation
- Update root README for V1 + V2 structure
- V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass
- Root is now clean for V2 development
2026-07-07 11:56:27 +07:00

393 lines
9.9 KiB
Go

package storage_test
import (
"context"
"os"
"path/filepath"
"testing"
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
)
func tempDir(t *testing.T) string {
t.Helper()
dir, err := os.MkdirTemp("", "storage-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
return dir
}
// ============ KeyPair CRUD ============
// 3.1 SaveKeyPair
func TestSaveKeyPair(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
kp := &models.KeyPair{
Name: "my-key",
Type: "ed25519",
PrivateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----",
}
err := store.SaveKeyPair(ctx, kp)
if err != nil {
t.Fatalf("SaveKeyPair failed: %v", err)
}
if kp.ID == "" {
t.Error("SaveKeyPair should generate UUID")
}
}
// 3.2 ListKeyPairs
func TestListKeyPairs(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
kp := &models.KeyPair{Name: "key1", Type: "ed25519", PrivateKey: "test-key-data"}
_ = store.SaveKeyPair(ctx, kp)
keys, err := store.ListKeyPairs(ctx)
if err != nil {
t.Fatalf("ListKeyPairs failed: %v", err)
}
if len(keys) != 1 {
t.Errorf("ListKeyPairs returned %d keys, want 1", len(keys))
}
}
// 3.3 GetKeyPair found
func TestGetKeyPairFound(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
kp := &models.KeyPair{Name: "key1", Type: "ed25519", PrivateKey: "test-key-data"}
_ = store.SaveKeyPair(ctx, kp)
found, err := store.GetKeyPair(ctx, kp.ID)
if err != nil {
t.Fatalf("GetKeyPair failed: %v", err)
}
if found.Name != "key1" {
t.Errorf("Name = %q, want %q", found.Name, "key1")
}
}
// 3.4 GetKeyPair not found
func TestGetKeyPairNotFound(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
_, err := store.GetKeyPair(ctx, "nonexistent")
if err == nil {
t.Error("GetKeyPair should return error for non-existent ID")
}
}
// 3.5 DeleteKeyPair
func TestDeleteKeyPair(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
kp := &models.KeyPair{Name: "key1", Type: "ed25519", PrivateKey: "test-key-data"}
_ = store.SaveKeyPair(ctx, kp)
err := store.DeleteKeyPair(ctx, kp.ID)
if err != nil {
t.Fatalf("DeleteKeyPair failed: %v", err)
}
keys, _ := store.ListKeyPairs(ctx)
if len(keys) != 0 {
t.Errorf("ListKeyPairs after delete returned %d keys, want 0", len(keys))
}
}
// 3.6 DeleteKeyPair not found
func TestDeleteKeyPairNotFound(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
err := store.DeleteKeyPair(ctx, "nonexistent")
if err == nil {
t.Error("DeleteKeyPair should return error for non-existent ID")
}
}
// ============ Snippet CRUD ============
// 3.7 SaveSnippet
func TestSaveSnippet(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
snippet := &models.Snippet{
Name: "deploy-script",
Command: "deploy.sh",
Description: "deployment script",
}
err := store.SaveSnippet(ctx, snippet)
if err != nil {
t.Fatalf("SaveSnippet failed: %v", err)
}
if snippet.ID == "" {
t.Error("SaveSnippet should generate UUID")
}
}
// 3.8 ListSnippets
func TestListSnippets(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
snippet := &models.Snippet{Name: "s1", Command: "cmd1", Description: "desc1"}
_ = store.SaveSnippet(ctx, snippet)
snippets, err := store.ListSnippets(ctx)
if err != nil {
t.Fatalf("ListSnippets failed: %v", err)
}
if len(snippets) != 1 {
t.Errorf("ListSnippets returned %d snippets, want 1", len(snippets))
}
}
// 3.9 GetSnippet found
func TestGetSnippetFound(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
snippet := &models.Snippet{Name: "s1", Command: "cmd1", Description: "desc1"}
_ = store.SaveSnippet(ctx, snippet)
found, err := store.GetSnippet(ctx, snippet.ID)
if err != nil {
t.Fatalf("GetSnippet failed: %v", err)
}
if found.Name != "s1" {
t.Errorf("Name = %q, want %q", found.Name, "s1")
}
}
// 3.10 GetSnippet not found
func TestGetSnippetNotFound(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
_, err := store.GetSnippet(ctx, "nonexistent")
if err == nil {
t.Error("GetSnippet should return error for non-existent ID")
}
}
// 3.11 DeleteSnippet
func TestDeleteSnippet(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
snippet := &models.Snippet{Name: "s1", Command: "cmd1", Description: "desc1"}
_ = store.SaveSnippet(ctx, snippet)
err := store.DeleteSnippet(ctx, snippet.ID)
if err != nil {
t.Fatalf("DeleteSnippet failed: %v", err)
}
snippets, _ := store.ListSnippets(ctx)
if len(snippets) != 0 {
t.Errorf("ListSnippets after delete returned %d snippets, want 0", len(snippets))
}
}
// 3.12 DeleteSnippet not found
func TestDeleteSnippetNotFound(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
err := store.DeleteSnippet(ctx, "nonexistent")
if err == nil {
t.Error("DeleteSnippet should return error for non-existent ID")
}
}
// ============ Encryption ============
// 3.13 SetPassword + SaveHost → encrypted on disk
func TestEncryptionSaveHost(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
store.SetPassword("test-password-123")
ctx := context.Background()
host := &models.Host{
Name: "encrypted-host",
Hostname: "192.168.1.100",
Port: 22,
Username: "admin",
}
err := store.SaveHost(ctx, host)
if err != nil {
t.Fatalf("SaveHost with encryption failed: %v", err)
}
// Read raw file — should be base64 ciphertext
data, _ := os.ReadFile(filepath.Join(dir, "hosts.json"))
if string(data) == "" {
t.Fatal("hosts.json should not be empty")
}
}
// 3.14 IsDataEncrypted
func TestIsDataEncrypted(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
// Before encryption — not encrypted
if store.IsDataEncrypted() {
t.Error("IsDataEncrypted should be false before SetPassword")
}
// After encryption
store.SetPassword("test-password-123")
ctx := context.Background()
host := &models.Host{Name: "h1", Hostname: "1.2.3.4", Port: 22, Username: "u"}
_ = store.SaveHost(ctx, host)
if !store.IsDataEncrypted() {
t.Error("IsDataEncrypted should be true after SaveHost with password")
}
}
// 3.15 Wrong password → error on load
func TestEncryptionWrongPassword(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
store.SetPassword("correct-password")
ctx := context.Background()
host := &models.Host{Name: "h1", Hostname: "1.2.3.4", Port: 22, Username: "u"}
_ = store.SaveHost(ctx, host)
// Try loading with wrong password
store2, _ := storage.NewJSONStorage(dir)
store2.SetPassword("wrong-password")
_, err := store2.ListHosts(ctx)
if err == nil {
t.Error("ListHosts with wrong password should return error")
}
}
// ============ MergeStrategy ============
// 3.16 MergeStrategyMerge
func TestMergeStrategyMerge(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
// Add existing host
existing := &models.Host{ID: "host-1", Name: "existing", Hostname: "1.1.1.1", Port: 22, Username: "u"}
_ = store.SaveHost(ctx, existing)
// Import with merge — new host should be added, existing kept
importData := &storage.ExportData{
Hosts: []*models.Host{
{ID: "host-2", Name: "imported", Hostname: "2.2.2.2", Port: 22, Username: "u"},
},
}
err := store.ImportData(ctx, importData, storage.MergeStrategyMerge)
if err != nil {
t.Fatalf("ImportData with merge failed: %v", err)
}
hosts, _ := store.ListHosts(ctx)
if len(hosts) != 2 {
t.Errorf("After merge, got %d hosts, want 2", len(hosts))
}
}
// 3.17 MergeStrategyReplace
func TestMergeStrategyReplace(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
// Add existing host
existing := &models.Host{ID: "host-1", Name: "existing", Hostname: "1.1.1.1", Port: 22, Username: "u"}
_ = store.SaveHost(ctx, existing)
// Import with replace — existing should be overwritten
importData := &storage.ExportData{
Hosts: []*models.Host{
{ID: "host-2", Name: "new", Hostname: "2.2.2.2", Port: 22, Username: "u"},
},
}
err := store.ImportData(ctx, importData, storage.MergeStrategyReplace)
if err != nil {
t.Fatalf("ImportData with replace failed: %v", err)
}
hosts, _ := store.ListHosts(ctx)
if len(hosts) != 1 {
t.Errorf("After replace, got %d hosts, want 1", len(hosts))
}
if hosts[0].ID != "host-2" {
t.Errorf("After replace, host ID = %q, want %q", hosts[0].ID, "host-2")
}
}
// ============ Edge Cases ============
// 3.18 SaveHost empty ID → generates UUID
func TestSaveHostEmptyID(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
ctx := context.Background()
host := &models.Host{Name: "no-id", Hostname: "1.2.3.4", Port: 22, Username: "u"}
err := store.SaveHost(ctx, host)
if err != nil {
t.Fatalf("SaveHost failed: %v", err)
}
if host.ID == "" {
t.Error("SaveHost should generate UUID for empty ID")
}
}
// GetPassword / IsEncrypted
func TestPasswordMethods(t *testing.T) {
dir := tempDir(t)
store, _ := storage.NewJSONStorage(dir)
if store.IsEncrypted() {
t.Error("IsEncrypted should be false initially")
}
if store.GetPassword() != "" {
t.Error("GetPassword should be empty initially")
}
store.SetPassword("test123")
if !store.IsEncrypted() {
t.Error("IsEncrypted should be true after SetPassword")
}
if store.GetPassword() != "test123" {
t.Errorf("GetPassword = %q, want %q", store.GetPassword(), "test123")
}
}