package v1 import ( "bytes" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gorilla/mux" "gopkg.in/yaml.v3" "github.com/wild-cloud/wild-central/daemon/internal/storage" ) func setupTestAPI(t *testing.T) (*API, string) { tmpDir := t.TempDir() appsDir := filepath.Join(tmpDir, "apps") api, err := NewAPI(tmpDir, appsDir) if err != nil { t.Fatalf("Failed to create test API: %v", err) } return api, tmpDir } func createTestInstance(t *testing.T, api *API, name string) { if err := api.instance.CreateInstance(name); err != nil { t.Fatalf("Failed to create test instance: %v", err) } } func TestUpdateYAMLFile_DeltaUpdate(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) // Create initial config initialConfig := map[string]interface{}{ "domain": "old.com", "email": "admin@old.com", "cluster": map[string]interface{}{ "name": "test-cluster", }, } initialYAML, _ := yaml.Marshal(initialConfig) if err := storage.WriteFile(configPath, initialYAML, 0644); err != nil { t.Fatalf("Failed to write initial config: %v", err) } // Update only domain updateData := map[string]interface{}{ "domain": "new.com", } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify merged config resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read result: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Failed to parse result: %v", err) } // Domain should be updated if result["domain"] != "new.com" { t.Errorf("Expected domain='new.com', got %v", result["domain"]) } // Email should be preserved if result["email"] != "admin@old.com" { t.Errorf("Expected email='admin@old.com', got %v", result["email"]) } // Cluster should be preserved if cluster, ok := result["cluster"].(map[string]interface{}); !ok { t.Errorf("Cluster not preserved as map") } else if cluster["name"] != "test-cluster" { t.Errorf("Cluster name not preserved") } } func TestUpdateYAMLFile_FullReplacement(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) // Create initial config initialConfig := map[string]interface{}{ "domain": "old.com", "email": "admin@old.com", "oldKey": "oldValue", } initialYAML, _ := yaml.Marshal(initialConfig) if err := storage.WriteFile(configPath, initialYAML, 0644); err != nil { t.Fatalf("Failed to write initial config: %v", err) } // Full replacement newConfig := map[string]interface{}{ "domain": "new.com", "email": "new@new.com", "newKey": "newValue", } newYAML, _ := yaml.Marshal(newConfig) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(newYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify result resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read result: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Failed to parse result: %v", err) } // All new values should be present if result["domain"] != "new.com" { t.Errorf("Expected domain='new.com', got %v", result["domain"]) } if result["email"] != "new@new.com" { t.Errorf("Expected email='new@new.com', got %v", result["email"]) } if result["newKey"] != "newValue" { t.Errorf("Expected newKey='newValue', got %v", result["newKey"]) } // Old key should still be present (shallow merge) if result["oldKey"] != "oldValue" { t.Errorf("Expected oldKey='oldValue', got %v", result["oldKey"]) } } func TestUpdateYAMLFile_NestedStructure(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) // Update with nested structure updateData := map[string]interface{}{ "cloud": map[string]interface{}{ "domain": "test.com", "dns": map[string]interface{}{ "ip": "1.2.3.4", "port": 53, }, }, } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify nested structure preserved resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read result: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Failed to parse result: %v", err) } // Verify nested structure is proper YAML, not Go map notation resultStr := string(resultData) if bytes.Contains(resultData, []byte("map[")) { t.Errorf("Result contains Go map notation: %s", resultStr) } // Verify structure is accessible cloud, ok := result["cloud"].(map[string]interface{}) if !ok { t.Fatalf("cloud is not a map: %T", result["cloud"]) } if cloud["domain"] != "test.com" { t.Errorf("Expected cloud.domain='test.com', got %v", cloud["domain"]) } dns, ok := cloud["dns"].(map[string]interface{}) if !ok { t.Fatalf("cloud.dns is not a map: %T", cloud["dns"]) } if dns["ip"] != "1.2.3.4" { t.Errorf("Expected dns.ip='1.2.3.4', got %v", dns["ip"]) } if dns["port"] != 53 { t.Errorf("Expected dns.port=53, got %v", dns["port"]) } } func TestUpdateYAMLFile_EmptyFileCreation(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) // Truncate the config file to make it empty (but still exists) if err := storage.WriteFile(configPath, []byte(""), 0644); err != nil { t.Fatalf("Failed to empty config file: %v", err) } // Update should populate empty file updateData := map[string]interface{}{ "domain": "new.com", "email": "admin@new.com", } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify content resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read result: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Failed to parse result: %v", err) } if result["domain"] != "new.com" { t.Errorf("Expected domain='new.com', got %v", result["domain"]) } if result["email"] != "admin@new.com" { t.Errorf("Expected email='admin@new.com', got %v", result["email"]) } } func TestUpdateYAMLFile_EmptyUpdate(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) // Create initial config initialConfig := map[string]interface{}{ "domain": "test.com", } initialYAML, _ := yaml.Marshal(initialConfig) if err := storage.WriteFile(configPath, initialYAML, 0644); err != nil { t.Fatalf("Failed to write initial config: %v", err) } // Empty update updateData := map[string]interface{}{} updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify file unchanged resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read result: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Failed to parse result: %v", err) } if result["domain"] != "test.com" { t.Errorf("Expected domain='test.com', got %v", result["domain"]) } } func TestUpdateYAMLFile_YAMLFormatting(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) // Update with complex nested structure updateData := map[string]interface{}{ "cloud": map[string]interface{}{ "domain": "test.com", "dns": map[string]interface{}{ "ip": "1.2.3.4", }, }, "cluster": map[string]interface{}{ "nodes": []interface{}{ map[string]interface{}{ "name": "node1", "ip": "10.0.0.1", }, map[string]interface{}{ "name": "node2", "ip": "10.0.0.2", }, }, }, } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify YAML formatting resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read result: %v", err) } resultStr := string(resultData) // Should not contain Go map notation if bytes.Contains(resultData, []byte("map[")) { t.Errorf("Result contains Go map notation: %s", resultStr) } // Should be valid YAML var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Result is not valid YAML: %v", err) } // Should have proper indentation (check for nested structure indicators) if !bytes.Contains(resultData, []byte(" ")) { t.Error("Result appears to lack proper indentation") } } func TestUpdateYAMLFile_InvalidYAML(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) // Send invalid YAML invalidYAML := []byte("invalid: yaml: content: [") req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(invalidYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } } func TestUpdateYAMLFile_InvalidInstance(t *testing.T) { api, _ := setupTestAPI(t) updateData := map[string]interface{}{ "domain": "test.com", } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/nonexistent/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": "nonexistent"} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusNotFound { t.Errorf("Expected status 404, got %d", w.Code) } } func TestUpdateYAMLFile_FilePermissions(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) updateData := map[string]interface{}{ "domain": "test.com", } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Check file permissions info, err := os.Stat(configPath) if err != nil { t.Fatalf("Failed to stat config file: %v", err) } expectedPerm := os.FileMode(0644) if info.Mode().Perm() != expectedPerm { t.Errorf("Expected permissions %v, got %v", expectedPerm, info.Mode().Perm()) } } func TestUpdateYAMLFile_UpdateSecrets(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) secretsPath := api.instance.GetInstanceSecretsPath(instanceName) // Update secrets updateData := map[string]interface{}{ "dbPassword": "secret123", "apiKey": "key456", } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/secrets", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateSecrets(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify secrets file created and contains data resultData, err := storage.ReadFile(secretsPath) if err != nil { t.Fatalf("Failed to read secrets: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Failed to parse secrets: %v", err) } if result["dbPassword"] != "secret123" { t.Errorf("Expected dbPassword='secret123', got %v", result["dbPassword"]) } if result["apiKey"] != "key456" { t.Errorf("Expected apiKey='key456', got %v", result["apiKey"]) } } func TestUpdateYAMLFile_ConcurrentUpdates(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) // This test verifies that file locking prevents race conditions // We'll simulate concurrent updates and verify data integrity numUpdates := 10 done := make(chan bool, numUpdates) for i := 0; i < numUpdates; i++ { go func(index int) { updateData := map[string]interface{}{ "counter": index, } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) done <- w.Code == http.StatusOK }(i) } // Wait for all updates to complete successCount := 0 for i := 0; i < numUpdates; i++ { if <-done { successCount++ } } if successCount != numUpdates { t.Errorf("Expected %d successful updates, got %d", numUpdates, successCount) } // Verify file is still valid YAML configPath := api.instance.GetInstanceConfigPath(instanceName) resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read final config: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Final config is not valid YAML: %v", err) } } func TestUpdateYAMLFile_PreservesComplexTypes(t *testing.T) { api, _ := setupTestAPI(t) instanceName := "test-instance" createTestInstance(t, api, instanceName) configPath := api.instance.GetInstanceConfigPath(instanceName) // Create config with various types updateData := map[string]interface{}{ "stringValue": "text", "intValue": 42, "floatValue": 3.14, "boolValue": true, "arrayValue": []interface{}{"a", "b", "c"}, "mapValue": map[string]interface{}{ "nested": "value", }, "nullValue": nil, } updateYAML, _ := yaml.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/v1/instances/"+instanceName+"/config", bytes.NewBuffer(updateYAML)) w := httptest.NewRecorder() vars := map[string]string{"name": instanceName} req = mux.SetURLVars(req, vars) api.UpdateConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify types preserved resultData, err := storage.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read result: %v", err) } var result map[string]interface{} if err := yaml.Unmarshal(resultData, &result); err != nil { t.Fatalf("Failed to parse result: %v", err) } if result["stringValue"] != "text" { t.Errorf("String value not preserved: %v", result["stringValue"]) } if result["intValue"] != 42 { t.Errorf("Int value not preserved: %v", result["intValue"]) } if result["floatValue"] != 3.14 { t.Errorf("Float value not preserved: %v", result["floatValue"]) } if result["boolValue"] != true { t.Errorf("Bool value not preserved: %v", result["boolValue"]) } arrayValue, ok := result["arrayValue"].([]interface{}) if !ok { t.Errorf("Array not preserved as slice: %T", result["arrayValue"]) } else if len(arrayValue) != 3 { t.Errorf("Array length not preserved: %d", len(arrayValue)) } mapValue, ok := result["mapValue"].(map[string]interface{}) if !ok { t.Errorf("Map not preserved: %T", result["mapValue"]) } else if mapValue["nested"] != "value" { t.Errorf("Nested map value not preserved: %v", mapValue["nested"]) } if result["nullValue"] != nil { t.Errorf("Null value not preserved: %v", result["nullValue"]) } }