package api import ( "bytes" "context" "encoding/json" "io" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "debtik/internal/auth" "debtik/internal/backup" ) func TestBackupAPIHandlers(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) _ = os.WriteFile(filepath.Join(debtikDir, "metadata.json"), []byte(`{"router": "test"}`), 0644) _ = os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), []byte(`auto lo`), 0644) backupMgr := backup.NewManager(debtikDir, netDir, storeDir, nil) s := &Server{ backupMgr: backupMgr, mux: http.NewServeMux(), fsHandler: http.NotFoundHandler(), } s.registerRoutes() // 1. Test Backup Export (Download) req := httptest.NewRequest(http.MethodGet, "/api/system/backup/export?comment=APITest", nil) // Context with full admin role ctx := context.WithValue(req.Context(), sessionContextKey, &auth.Session{ Username: "admin", Role: auth.RoleFull, }) req = req.WithContext(ctx) rec := httptest.NewRecorder() s.mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("Expected 200 OK from backup export, got %d: %s", rec.Code, rec.Body.String()) } if rec.Header().Get("Content-Type") != "application/zip" { t.Errorf("Expected application/zip Content-Type, got %s", rec.Header().Get("Content-Type")) } zipBytes := rec.Body.Bytes() if len(zipBytes) == 0 { t.Fatal("Exported zip is empty") } // 2. Test Backup Inspect via multipart upload bodyBuf := new(bytes.Buffer) mpWriter := multipart.NewWriter(bodyBuf) part, err := mpWriter.CreateFormFile("backup", "test-backup.zip") if err != nil { t.Fatal(err) } _, _ = io.Copy(part, bytes.NewReader(zipBytes)) _ = mpWriter.Close() inspectReq := httptest.NewRequest(http.MethodPost, "/api/system/backup/inspect", bodyBuf) inspectReq.Header.Set("Content-Type", mpWriter.FormDataContentType()) inspectReq = inspectReq.WithContext(ctx) inspectRec := httptest.NewRecorder() s.mux.ServeHTTP(inspectRec, inspectReq) if inspectRec.Code != http.StatusOK { t.Fatalf("Expected 200 OK from backup inspect, got %d: %s", inspectRec.Code, inspectRec.Body.String()) } var inspectResp struct { Success bool `json:"success"` Manifest *backup.Manifest `json:"manifest"` } if err := json.Unmarshal(inspectRec.Body.Bytes(), &inspectResp); err != nil { t.Fatalf("Failed unmarshaling inspect response: %v", err) } if !inspectResp.Success || inspectResp.Manifest == nil { t.Fatal("Expected success and manifest in inspect response") } if inspectResp.Manifest.Comment != "APITest" { t.Errorf("Manifest comment mismatch: %s != APITest", inspectResp.Manifest.Comment) } // 3. Test Backup Restore restoreBuf := new(bytes.Buffer) mpWriter2 := multipart.NewWriter(restoreBuf) part2, _ := mpWriter2.CreateFormFile("backup", "restore-backup.zip") _, _ = io.Copy(part2, bytes.NewReader(zipBytes)) _ = mpWriter2.Close() restoreReq := httptest.NewRequest(http.MethodPost, "/api/system/backup/restore", restoreBuf) restoreReq.Header.Set("Content-Type", mpWriter2.FormDataContentType()) restoreReq = restoreReq.WithContext(ctx) restoreRec := httptest.NewRecorder() s.mux.ServeHTTP(restoreRec, restoreReq) if restoreRec.Code != http.StatusOK { t.Fatalf("Expected 200 OK from backup restore, got %d: %s", restoreRec.Code, restoreRec.Body.String()) } var restoreRes backup.RestoreResult if err := json.Unmarshal(restoreRec.Body.Bytes(), &restoreRes); err != nil { t.Fatalf("Failed unmarshaling restore response: %v", err) } if !restoreRes.Success { t.Error("Expected restore success true") } // 4. Test Backup List (should contain pre-restore safety backup) listReq := httptest.NewRequest(http.MethodGet, "/api/system/backups", nil) listReq = listReq.WithContext(ctx) listRec := httptest.NewRecorder() s.mux.ServeHTTP(listRec, listReq) if listRec.Code != http.StatusOK { t.Fatalf("Expected 200 OK from backup list, got %d: %s", listRec.Code, listRec.Body.String()) } var backupsList []backup.BackupInfo if err := json.Unmarshal(listRec.Body.Bytes(), &backupsList); err != nil { t.Fatalf("Failed unmarshaling backup list: %v", err) } if len(backupsList) == 0 { t.Error("Expected at least 1 backup in list") } // 5. Test Forbidden for Read-Only user roCtx := context.WithValue(req.Context(), sessionContextKey, &auth.Session{ Username: "readonly-user", Role: auth.RoleRead, }) roReq := httptest.NewRequest(http.MethodGet, "/api/system/backup/export", nil) roReq = roReq.WithContext(roCtx) roRec := httptest.NewRecorder() s.mux.ServeHTTP(roRec, roReq) if roRec.Code != http.StatusForbidden { t.Errorf("Expected 403 Forbidden for read-only user, got %d", roRec.Code) } }