package backup import ( "archive/zip" "bytes" "os" "path/filepath" "strings" "testing" ) func TestCreateAndInspectBackup(t *testing.T) { tempDir := t.TempDir() debtikDir := filepath.Join(tempDir, "debtik") netDir := filepath.Join(tempDir, "network") storeDir := filepath.Join(tempDir, "backups") // Create test structure if err := os.MkdirAll(filepath.Join(debtikDir, "certs"), 0755); err != nil { t.Fatal(err) } if err := os.MkdirAll(netDir, 0755); err != nil { t.Fatal(err) } metadataContent := []byte(`{"version": 1, "description": "test"}`) keyContent := []byte(`super-secret-ssh-key`) certContent := []byte(`fake-cert-data`) ifacesContent := []byte(`auto eth0` + "\n" + `iface eth0 inet dhcp`) if err := os.WriteFile(filepath.Join(debtikDir, "metadata.json"), metadataContent, 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(debtikDir, "ssh_host_ed25519_key"), keyContent, 0600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(debtikDir, "certs", "server.crt"), certContent, 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), ifacesContent, 0644); err != nil { t.Fatal(err) } mgr := NewManager(debtikDir, netDir, storeDir, nil) // Create backup zipBytes, manifest, err := mgr.CreateBackup("Unit test backup") if err != nil { t.Fatalf("CreateBackup failed: %v", err) } if len(zipBytes) == 0 { t.Fatal("Expected non-empty zip bytes") } if manifest.Comment != "Unit test backup" { t.Errorf("Unexpected manifest comment: %s", manifest.Comment) } if len(manifest.Files) != 4 { t.Fatalf("Expected 4 files in manifest, got %d", len(manifest.Files)) } // Inspect backup r := bytes.NewReader(zipBytes) inspected, err := mgr.InspectBackup(r, int64(len(zipBytes))) if err != nil { t.Fatalf("InspectBackup failed: %v", err) } if inspected.Hostname != manifest.Hostname { t.Errorf("Hostname mismatch: %s != %s", inspected.Hostname, manifest.Hostname) } if len(inspected.Files) != 4 { t.Errorf("Expected 4 files in inspected manifest, got %d", len(inspected.Files)) } } func TestRestoreBackup(t *testing.T) { tempDir := t.TempDir() debtikDir := filepath.Join(tempDir, "debtik") netDir := filepath.Join(tempDir, "network") storeDir := filepath.Join(tempDir, "backups") _ = os.MkdirAll(debtikDir, 0755) _ = os.MkdirAll(netDir, 0755) origMetadata := []byte(`{"version": "orig"}`) origKey := []byte(`private-key-123`) origIfaces := []byte(`auto eth0`) _ = os.WriteFile(filepath.Join(debtikDir, "metadata.json"), origMetadata, 0644) _ = os.WriteFile(filepath.Join(debtikDir, "ssh_host_ed25519_key"), origKey, 0600) _ = os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), origIfaces, 0644) restarted := false restartHook := func() error { restarted = true return nil } mgr := NewManager(debtikDir, netDir, storeDir, restartHook) _ = restarted // 1. Create backup zipBytes, _, err := mgr.CreateBackup("Pre-modify backup") if err != nil { t.Fatalf("CreateBackup failed: %v", err) } // 2. Modify live files _ = os.WriteFile(filepath.Join(debtikDir, "metadata.json"), []byte(`{"version": "modified"}`), 0644) _ = os.WriteFile(filepath.Join(debtikDir, "rogue.json"), []byte(`rogue data`), 0644) _ = os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), []byte(`modified eth0`), 0644) // 3. Restore backup r := bytes.NewReader(zipBytes) res, err := mgr.RestoreBackup(r, int64(len(zipBytes))) if err != nil { t.Fatalf("RestoreBackup failed: %v", err) } if !res.Success { t.Fatal("Expected restore success true") } // 4. Verify original metadata was restored restoredMeta, err := os.ReadFile(filepath.Join(debtikDir, "metadata.json")) if err != nil { t.Fatalf("Failed reading restored metadata: %v", err) } if string(restoredMeta) != string(origMetadata) { t.Errorf("Metadata mismatch: got %q, want %q", string(restoredMeta), string(origMetadata)) } // 5. Verify rogue file was removed during sync if _, err := os.Stat(filepath.Join(debtikDir, "rogue.json")); !os.IsNotExist(err) { t.Error("Expected rogue.json to be cleaned up after restore") } // 6. Verify key permissions are 0600 keyInfo, err := os.Stat(filepath.Join(debtikDir, "ssh_host_ed25519_key")) if err != nil { t.Fatalf("Failed reading restored key: %v", err) } if keyInfo.Mode().Perm() != 0600 { t.Errorf("Expected 0600 perm on private key, got %v", keyInfo.Mode().Perm()) } // 7. Verify pre-restore fallback backup exists in storeDir if res.SafetyBackup == "" { t.Error("Expected non-empty SafetyBackup path") } if _, err := os.Stat(res.SafetyBackup); err != nil { t.Errorf("Safety backup file does not exist: %v", err) } } func TestZipSlipPrevention(t *testing.T) { tempDir := t.TempDir() debtikDir := filepath.Join(tempDir, "debtik") netDir := filepath.Join(tempDir, "network") storeDir := filepath.Join(tempDir, "backups") mgr := NewManager(debtikDir, netDir, storeDir, nil) // Create malicious zip with traversal path buf := new(bytes.Buffer) zw := zip.NewWriter(buf) // Valid manifest mf, err := zw.Create("manifest.json") if err != nil { t.Fatal(err) } _, _ = mf.Write([]byte(`{"version": "1.0", "files": []}`)) // Malicious traversal file malicious, err := zw.Create("../../../etc/evil.conf") if err != nil { t.Fatal(err) } _, _ = malicious.Write([]byte(`malicious payload`)) _ = zw.Close() r := bytes.NewReader(buf.Bytes()) _, err = mgr.InspectBackup(r, int64(buf.Len())) if err == nil { t.Fatal("Expected InspectBackup to fail on Zip Slip traversal entry") } if !strings.Contains(err.Error(), "illegal entry path") { t.Errorf("Expected illegal entry path error, got %v", err) } // Attempt restore _, err = mgr.RestoreBackup(r, int64(buf.Len())) if err == nil { t.Fatal("Expected RestoreBackup to fail on Zip Slip entry") } } func TestLocalBackupsAndPruning(t *testing.T) { tempDir := t.TempDir() debtikDir := filepath.Join(tempDir, "debtik") netDir := filepath.Join(tempDir, "network") storeDir := filepath.Join(tempDir, "backups") _ = os.MkdirAll(debtikDir, 0755) _ = os.WriteFile(filepath.Join(debtikDir, "dummy.json"), []byte("{}"), 0644) mgr := NewManager(debtikDir, netDir, storeDir, nil) // Create 7 pre-restore backups for i := 0; i < 7; i++ { _, err := mgr.CreatePreRestoreBackup() if err != nil { t.Fatalf("Failed creating pre-restore backup %d: %v", i, err) } } // List backups list, err := mgr.ListLocalBackups() if err != nil { t.Fatalf("ListLocalBackups failed: %v", err) } // Pre-restore backups should be pruned to keep 5 if len(list) != 5 { t.Errorf("Expected 5 backups after pruning, got %d", len(list)) } // Test GetLocalBackup savedFile := list[0].Filename path, err := mgr.GetLocalBackup(savedFile) if err != nil { t.Fatalf("GetLocalBackup failed: %v", err) } if _, err := os.Stat(path); err != nil { t.Fatalf("File does not exist: %v", err) } // Test GetLocalBackup traversal reject _, err = mgr.GetLocalBackup("../../evil.zip") if err == nil { t.Error("Expected error on traversal filename") } }