// ABOUTME: Unit tests for AppInstance management APIs using httptest mock server // ABOUTME: Tests create, show, list, refresh, and delete operations with error conditions package edgeconnect import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCreateAppInstance(t *testing.T) { tests := []struct { name string input *NewAppInstanceInput mockStatusCode int mockResponse string expectError bool errorContains string }{ { name: "successful creation", input: &NewAppInstanceInput{ Region: "us-west", AppInst: AppInstance{ Key: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, AppKey: AppKey{ Organization: "testorg", Name: "testapp", Version: "1.0.0", }, Flavor: Flavor{Name: "m4.small"}, }, }, mockStatusCode: 200, mockResponse: `{"message": "success"}`, expectError: false, }, { name: "validation error", input: &NewAppInstanceInput{ Region: "us-west", AppInst: AppInstance{ Key: AppInstanceKey{ Organization: "", Name: "testinst", }, }, }, mockStatusCode: 400, mockResponse: `{"message": "organization is required"}`, expectError: true, }, { name: "HTTP 200 with CreateError message", input: &NewAppInstanceInput{ Region: "us-west", AppInst: AppInstance{ Key: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, Flavor: Flavor{Name: "m4.small"}, }, }, mockStatusCode: 200, mockResponse: `{"data":{"message":"Creating"}} {"data":{"message":"a service has been configured"}} {"data":{"message":"CreateError"}} {"data":{"message":"Deleting AppInst due to failure"}} {"data":{"message":"Deleted AppInst successfully"}}`, expectError: true, errorContains: "CreateError", }, { name: "HTTP 200 with result error code", input: &NewAppInstanceInput{ Region: "us-west", AppInst: AppInstance{ Key: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, Flavor: Flavor{Name: "m4.small"}, }, }, mockStatusCode: 200, mockResponse: `{"data":{"message":"Creating"}} {"data":{"message":"a service has been configured"}} {"data":{"message":"CreateError"}} {"data":{"message":"Deleting AppInst due to failure"}} {"data":{"message":"Deleted AppInst successfully"}} {"result":{"message":"Encountered failures: Create App Inst failed: deployments.apps is forbidden: User \"system:serviceaccount:edgexr:crm-telekomop-munich\" cannot create resource \"deployments\" in API group \"apps\" in the namespace \"gitea\"","code":400}}`, expectError: true, errorContains: "deployments.apps is forbidden", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/api/v1/auth/ctrl/CreateAppInst", r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(tt.mockStatusCode) _, _ = w.Write([]byte(tt.mockResponse)) })) defer server.Close() // Create client client := NewClient(server.URL, WithHTTPClient(&http.Client{Timeout: 5 * time.Second}), WithAuthProvider(NewStaticTokenProvider("test-token")), ) // Execute test ctx := context.Background() err := client.CreateAppInstance(ctx, tt.input) // Verify results if tt.expectError { assert.Error(t, err) if tt.errorContains != "" { assert.Contains(t, err.Error(), tt.errorContains) } } else { assert.NoError(t, err) } }) } } func TestShowAppInstance(t *testing.T) { tests := []struct { name string appKey AppKey appInstKey AppInstanceKey region string mockStatusCode int mockResponse string expectError bool expectNotFound bool }{ { name: "successful show", appInstKey: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, appKey: AppKey{Name: "test-app-id"}, region: "us-west", mockStatusCode: 200, mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}} `, expectError: false, expectNotFound: false, }, { name: "instance not found", appInstKey: AppInstanceKey{ Organization: "testorg", Name: "nonexistent", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, appKey: AppKey{Name: "test-app-id"}, region: "us-west", mockStatusCode: 404, mockResponse: "", expectError: true, expectNotFound: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/api/v1/auth/ctrl/ShowAppInst", r.URL.Path) w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { _, _ = w.Write([]byte(tt.mockResponse)) } })) defer server.Close() // Create client client := NewClient(server.URL, WithHTTPClient(&http.Client{Timeout: 5 * time.Second}), ) // Execute test ctx := context.Background() appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.appKey, tt.region) // Verify results if tt.expectError { assert.Error(t, err) if tt.expectNotFound { assert.Contains(t, err.Error(), "resource not found") } } else { require.NoError(t, err) assert.Equal(t, tt.appInstKey.Organization, appInst.Key.Organization) assert.Equal(t, tt.appInstKey.Name, appInst.Key.Name) assert.Equal(t, "Ready", appInst.State) } }) } } func TestShowAppInstances(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/api/v1/auth/ctrl/ShowAppInst", r.URL.Path) // Verify request body var filter AppInstanceFilter err := json.NewDecoder(r.Body).Decode(&filter) require.NoError(t, err) assert.Equal(t, "testorg", filter.AppInstance.Key.Organization) assert.Equal(t, "us-west", filter.Region) // Return multiple app instances response := `{"data": {"key": {"organization": "testorg", "name": "inst1"}, "state": "Ready"}} {"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}} ` w.WriteHeader(200) _, _ = w.Write([]byte(response)) })) defer server.Close() client := NewClient(server.URL) ctx := context.Background() appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, AppKey{}, "us-west") require.NoError(t, err) assert.Len(t, appInstances, 2) assert.Equal(t, "inst1", appInstances[0].Key.Name) assert.Equal(t, "Ready", appInstances[0].State) assert.Equal(t, "inst2", appInstances[1].Key.Name) assert.Equal(t, "Creating", appInstances[1].State) } func TestUpdateAppInstance(t *testing.T) { tests := []struct { name string input *UpdateAppInstanceInput mockStatusCode int mockResponse string expectError bool }{ { name: "successful update", input: &UpdateAppInstanceInput{ Region: "us-west", AppInst: AppInstance{ Key: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, AppKey: AppKey{ Organization: "testorg", Name: "testapp", Version: "1.0.0", }, Flavor: Flavor{Name: "m4.medium"}, PowerState: "PowerOn", }, }, mockStatusCode: 200, mockResponse: `{"message": "success"}`, expectError: false, }, { name: "validation error", input: &UpdateAppInstanceInput{ Region: "us-west", AppInst: AppInstance{ Key: AppInstanceKey{ Organization: "", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, }, }, mockStatusCode: 400, mockResponse: `{"message": "organization is required"}`, expectError: true, }, { name: "instance not found", input: &UpdateAppInstanceInput{ Region: "us-west", AppInst: AppInstance{ Key: AppInstanceKey{ Organization: "testorg", Name: "nonexistent", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, }, }, mockStatusCode: 404, mockResponse: `{"message": "app instance not found"}`, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/api/v1/auth/ctrl/UpdateAppInst", r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) // Verify request body var input UpdateAppInstanceInput err := json.NewDecoder(r.Body).Decode(&input) require.NoError(t, err) assert.Equal(t, tt.input.Region, input.Region) assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization) w.WriteHeader(tt.mockStatusCode) _, _ = w.Write([]byte(tt.mockResponse)) })) defer server.Close() // Create client client := NewClient(server.URL, WithHTTPClient(&http.Client{Timeout: 5 * time.Second}), WithAuthProvider(NewStaticTokenProvider("test-token")), ) // Execute test ctx := context.Background() err := client.UpdateAppInstance(ctx, tt.input) // Verify results if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestRefreshAppInstance(t *testing.T) { tests := []struct { name string appInstKey AppInstanceKey region string mockStatusCode int expectError bool }{ { name: "successful refresh", appInstKey: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, region: "us-west", mockStatusCode: 200, expectError: false, }, { name: "server error", appInstKey: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, region: "us-west", mockStatusCode: 500, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/api/v1/auth/ctrl/RefreshAppInst", r.URL.Path) w.WriteHeader(tt.mockStatusCode) })) defer server.Close() client := NewClient(server.URL) ctx := context.Background() err := client.RefreshAppInstance(ctx, tt.appInstKey, tt.region) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestDeleteAppInstance(t *testing.T) { tests := []struct { name string appInstKey AppInstanceKey region string mockStatusCode int expectError bool }{ { name: "successful deletion", appInstKey: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, region: "us-west", mockStatusCode: 200, expectError: false, }, { name: "already deleted (404 ok)", appInstKey: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, region: "us-west", mockStatusCode: 404, expectError: false, }, { name: "server error", appInstKey: AppInstanceKey{ Organization: "testorg", Name: "testinst", CloudletKey: CloudletKey{ Organization: "cloudletorg", Name: "testcloudlet", }, }, region: "us-west", mockStatusCode: 500, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/api/v1/auth/ctrl/DeleteAppInst", r.URL.Path) w.WriteHeader(tt.mockStatusCode) })) defer server.Close() client := NewClient(server.URL) ctx := context.Background() err := client.DeleteAppInstance(ctx, tt.appInstKey, tt.region) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) } }) } }