v0.22.0: First-run setup wizard, local infra backup, hub verification
New controller features:
- Web-based setup wizard replaces docker-setup.sh interactive config
- Dual listener: :8080 (Traefik) + :8081 (direct HTTP for LAN)
- Drive scanner finds .felhom-infra-backup/ on all block devices
- Hub recovery pull (GET /api/v1/recovery/{id}) with retrieval password
- Fresh install: Hub config download or manual wizard
- CSRF protection, state persistence, Hungarian UI
- Local infra backup written to all connected drives after each backup cycle
- .felhom-infra-backup/backup.json + metadata.json with SHA256 checksum
- Hub verification: parse customer_blocked from report push response
- Limited mode after 7 days without verification
- Recovery info page on Settings + recovery-info.txt file generation
- Pending events queue: DR events sent to Hub on next report push
- docker-setup.sh v6.0.0: removed interactive wizard, minimal controller.yaml only
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteAndReadLocalInfraBackup(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
drive := filepath.Join(tmpDir, "mnt", "hdd_0")
|
||||
if err := os.MkdirAll(drive, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
backupJSON := []byte(`{"customer_id":"test-123","domain":"test.hu","controller_version":"v0.21.0","timestamp":"2026-02-21T10:00:00Z"}`)
|
||||
logger := testLogger(t)
|
||||
|
||||
WriteLocalInfraBackup(backupJSON, "test-123", "v0.21.0", "2026-02-21T10:00:00Z", []string{drive}, logger)
|
||||
|
||||
// Verify files exist
|
||||
dir := InfraBackupDir(drive)
|
||||
if _, err := os.Stat(filepath.Join(dir, "backup.json")); err != nil {
|
||||
t.Fatalf("backup.json not found: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "metadata.json")); err != nil {
|
||||
t.Fatalf("metadata.json not found: %v", err)
|
||||
}
|
||||
|
||||
// Read back
|
||||
data, meta, err := ReadLocalInfraBackup(drive)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadLocalInfraBackup failed: %v", err)
|
||||
}
|
||||
if string(data) != string(backupJSON) {
|
||||
t.Errorf("backup data mismatch: got %s", string(data))
|
||||
}
|
||||
if meta.SchemaVersion != 1 {
|
||||
t.Errorf("expected schema version 1, got %d", meta.SchemaVersion)
|
||||
}
|
||||
if meta.CustomerID != "test-123" {
|
||||
t.Errorf("expected customer_id test-123, got %s", meta.CustomerID)
|
||||
}
|
||||
if meta.ControllerVersion != "v0.21.0" {
|
||||
t.Errorf("expected controller version v0.21.0, got %s", meta.ControllerVersion)
|
||||
}
|
||||
|
||||
// Verify checksum
|
||||
hash := sha256.Sum256(backupJSON)
|
||||
expected := hex.EncodeToString(hash[:])
|
||||
if meta.Checksum != expected {
|
||||
t.Errorf("checksum mismatch: expected %s, got %s", expected, meta.Checksum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLocalInfraBackup_ChecksumMismatch(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
drive := filepath.Join(tmpDir, "mnt", "hdd_0")
|
||||
dir := InfraBackupDir(drive)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write valid metadata with wrong checksum
|
||||
meta := InfraMetadata{SchemaVersion: 1, Checksum: "0000000000000000000000000000000000000000000000000000000000000000"}
|
||||
metaJSON, _ := json.Marshal(meta)
|
||||
os.WriteFile(filepath.Join(dir, "metadata.json"), metaJSON, 0600)
|
||||
os.WriteFile(filepath.Join(dir, "backup.json"), []byte(`{"test":true}`), 0600)
|
||||
|
||||
_, _, err := ReadLocalInfraBackup(drive)
|
||||
if err == nil {
|
||||
t.Fatal("expected checksum mismatch error")
|
||||
}
|
||||
if got := err.Error(); !contains(got, "checksum mismatch") {
|
||||
t.Errorf("expected checksum mismatch error, got: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLocalInfraBackup_SchemaVersionTooNew(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
drive := filepath.Join(tmpDir, "mnt", "hdd_0")
|
||||
dir := InfraBackupDir(drive)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
meta := InfraMetadata{SchemaVersion: 999}
|
||||
metaJSON, _ := json.Marshal(meta)
|
||||
os.WriteFile(filepath.Join(dir, "metadata.json"), metaJSON, 0600)
|
||||
os.WriteFile(filepath.Join(dir, "backup.json"), []byte(`{}`), 0600)
|
||||
|
||||
_, _, err := ReadLocalInfraBackup(drive)
|
||||
if err == nil {
|
||||
t.Fatal("expected schema version error")
|
||||
}
|
||||
if got := err.Error(); !contains(got, "newer than supported") {
|
||||
t.Errorf("expected schema version error, got: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLocalInfraBackup_MissingFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_, _, err := ReadLocalInfraBackup(tmpDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing files")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteLocalInfraBackup_MultipleDrives(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
drives := []string{
|
||||
filepath.Join(tmpDir, "drive1"),
|
||||
filepath.Join(tmpDir, "drive2"),
|
||||
filepath.Join(tmpDir, "drive3_fail"), // won't be created as a dir, but MkdirAll should handle it
|
||||
}
|
||||
for _, d := range drives {
|
||||
os.MkdirAll(d, 0755)
|
||||
}
|
||||
|
||||
backupJSON := []byte(`{"test":"multi"}`)
|
||||
logger := testLogger(t)
|
||||
|
||||
WriteLocalInfraBackup(backupJSON, "multi-test", "v1.0", "2026-01-01T00:00:00Z", drives, logger)
|
||||
|
||||
// All 3 should succeed
|
||||
for _, d := range drives {
|
||||
data, _, err := ReadLocalInfraBackup(d)
|
||||
if err != nil {
|
||||
t.Errorf("drive %s: read failed: %v", d, err)
|
||||
continue
|
||||
}
|
||||
if string(data) != string(backupJSON) {
|
||||
t.Errorf("drive %s: data mismatch", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteLocalInfraBackup_NoDrives(t *testing.T) {
|
||||
logger := testLogger(t)
|
||||
// Should not panic
|
||||
WriteLocalInfraBackup([]byte(`{}`), "test", "v1.0", "2026-01-01T00:00:00Z", nil, logger)
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr))
|
||||
}
|
||||
|
||||
func containsStr(s, substr string) bool {
|
||||
for i := 0; i+len(substr) <= len(s); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func testLogger(t *testing.T) *log.Logger {
|
||||
return log.New(os.Stderr, "[test] ", log.LstdFlags)
|
||||
}
|
||||
Reference in New Issue
Block a user