github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/operation/api_test.go (about)

     1  /*
     2   * Copyright 2020 The Compass Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package operation_test
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"path"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/kyma-incubator/compass/components/director/internal/model"
    30  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    31  
    32  	"github.com/gorilla/mux"
    33  
    34  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    35  	"github.com/kyma-incubator/compass/components/director/pkg/operation"
    36  	"github.com/kyma-incubator/compass/components/director/pkg/persistence/txtest"
    37  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    38  	"github.com/stretchr/testify/require"
    39  )
    40  
    41  const (
    42  	tenantID   = "d6f1d2bb-62f5-4971-9efe-8af93d6528a7"
    43  	resourceID = "46eb9542-8b18-4e4d-96d1-67f7e9675bb2"
    44  )
    45  
    46  func TestServeHTTP(t *testing.T) {
    47  	t.Run("when tenant is missing it should return internal server error", func(t *testing.T) {
    48  		writer := httptest.NewRecorder()
    49  		req := fixEmptyRequest(t, context.Background(), string(resource.Application), resourceID)
    50  
    51  		handler := operation.NewHandler(nil, nil, func(ctx context.Context) (string, error) {
    52  			return "", mockedError()
    53  		})
    54  		handler.ServeHTTP(writer, req)
    55  
    56  		require.Contains(t, writer.Body.String(), "Unable to determine tenant for request")
    57  		require.Equal(t, http.StatusInternalServerError, writer.Code)
    58  	})
    59  
    60  	t.Run("when resourceID and resourceType are missing it should return bad request", func(t *testing.T) {
    61  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
    62  
    63  		writer := httptest.NewRecorder()
    64  		req := fixEmptyRequest(t, ctx, "", "")
    65  
    66  		handler := operation.NewHandler(nil, nil, loadTenantFunc)
    67  		handler.ServeHTTP(writer, req)
    68  
    69  		require.Contains(t, writer.Body.String(), "Unexpected resource type and/or GUID")
    70  		require.Equal(t, http.StatusBadRequest, writer.Code)
    71  	})
    72  
    73  	t.Run("when resource ID is not GUID it should return bad request", func(t *testing.T) {
    74  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
    75  
    76  		writer := httptest.NewRecorder()
    77  		req := fixEmptyRequest(t, ctx, string(resource.Application), "123")
    78  
    79  		handler := operation.NewHandler(nil, nil, loadTenantFunc)
    80  		handler.ServeHTTP(writer, req)
    81  
    82  		require.Contains(t, writer.Body.String(), "Unexpected resource type and/or GUID")
    83  		require.Equal(t, http.StatusBadRequest, writer.Code)
    84  	})
    85  
    86  	t.Run("when resource type is not application it should return bad request", func(t *testing.T) {
    87  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
    88  
    89  		writer := httptest.NewRecorder()
    90  		req := fixEmptyRequest(t, ctx, string(resource.Runtime), resourceID)
    91  
    92  		queryValues := req.URL.Query()
    93  		queryValues.Add(operation.ResourceTypeParam, "runtime")
    94  
    95  		req.URL.RawQuery = queryValues.Encode()
    96  
    97  		handler := operation.NewHandler(nil, nil, loadTenantFunc)
    98  		handler.ServeHTTP(writer, req)
    99  
   100  		require.Contains(t, writer.Body.String(), "Unexpected resource type and/or GUID")
   101  		require.Equal(t, http.StatusBadRequest, writer.Code)
   102  	})
   103  
   104  	t.Run("when transaction fails to begin it should return internal server error", func(t *testing.T) {
   105  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
   106  
   107  		writer := httptest.NewRecorder()
   108  		req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID)
   109  
   110  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnBegin()
   111  		defer mockedTx.AssertExpectations(t)
   112  		defer mockedTransactioner.AssertExpectations(t)
   113  
   114  		handler := operation.NewHandler(mockedTransactioner, nil, loadTenantFunc)
   115  		handler.ServeHTTP(writer, req)
   116  
   117  		require.Equal(t, http.StatusInternalServerError, writer.Code)
   118  		require.Contains(t, writer.Body.String(), "Unable to establish connection with database")
   119  	})
   120  
   121  	t.Run("when resource fetcher func fails to fetch missing application resource it should return not found", func(t *testing.T) {
   122  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
   123  
   124  		writer := httptest.NewRecorder()
   125  		req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID)
   126  
   127  		queryValues := req.URL.Query()
   128  		queryValues.Add(operation.ResourceIDParam, resourceID)
   129  		queryValues.Add(operation.ResourceTypeParam, string(resource.Application))
   130  
   131  		req.URL.RawQuery = queryValues.Encode()
   132  
   133  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatDoesntExpectCommit()
   134  		defer mockedTx.AssertExpectations(t)
   135  		defer mockedTransactioner.AssertExpectations(t)
   136  
   137  		handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) {
   138  			return nil, apperrors.NewNotFoundError(resource.Application, resourceID)
   139  		}, loadTenantFunc)
   140  		handler.ServeHTTP(writer, req)
   141  
   142  		require.Equal(t, http.StatusNotFound, writer.Code)
   143  		require.Contains(t, writer.Body.String(), "Object not found")
   144  	})
   145  
   146  	t.Run("when resource fetcher func fails to fetch application it should return internal server error", func(t *testing.T) {
   147  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
   148  
   149  		writer := httptest.NewRecorder()
   150  		req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID)
   151  
   152  		queryValues := req.URL.Query()
   153  		queryValues.Add(operation.ResourceIDParam, resourceID)
   154  		queryValues.Add(operation.ResourceTypeParam, string(resource.Application))
   155  
   156  		req.URL.RawQuery = queryValues.Encode()
   157  
   158  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatDoesntExpectCommit()
   159  		defer mockedTx.AssertExpectations(t)
   160  		defer mockedTransactioner.AssertExpectations(t)
   161  
   162  		handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) {
   163  			return nil, mockedError()
   164  		}, loadTenantFunc)
   165  		handler.ServeHTTP(writer, req)
   166  
   167  		require.Equal(t, http.StatusInternalServerError, writer.Code)
   168  		require.Contains(t, writer.Body.String(), "Unable to execute database operation")
   169  	})
   170  
   171  	t.Run("when transaction fails to commit it should return internal server error ", func(t *testing.T) {
   172  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
   173  
   174  		writer := httptest.NewRecorder()
   175  		req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID)
   176  
   177  		queryValues := req.URL.Query()
   178  		queryValues.Add(operation.ResourceIDParam, resourceID)
   179  		queryValues.Add(operation.ResourceTypeParam, string(resource.Application))
   180  
   181  		req.URL.RawQuery = queryValues.Encode()
   182  
   183  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnCommit()
   184  		defer mockedTx.AssertExpectations(t)
   185  		defer mockedTransactioner.AssertExpectations(t)
   186  
   187  		handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) {
   188  			return nil, nil
   189  		}, loadTenantFunc)
   190  		handler.ServeHTTP(writer, req)
   191  
   192  		require.Equal(t, http.StatusInternalServerError, writer.Code)
   193  		require.Contains(t, writer.Body.String(), "Unable to finalize database operation")
   194  	})
   195  
   196  	t.Run("when application is successfully fetched it should return a respective operation", func(t *testing.T) {
   197  		ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID)
   198  
   199  		req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID)
   200  
   201  		mockedErr := mockedError().Error()
   202  		now := time.Now()
   203  		type testCase struct {
   204  			Name             string
   205  			Application      *model.Application
   206  			ExpectedResponse operation.OperationResponse
   207  		}
   208  
   209  		cases := []testCase{
   210  			{
   211  				Name:        "Successful CREATE Operation",
   212  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &time.Time{}, DeletedAt: &time.Time{}, Ready: true}},
   213  				ExpectedResponse: operation.OperationResponse{
   214  					Operation: &operation.Operation{
   215  						ResourceID:    resourceID,
   216  						ResourceType:  resource.Application,
   217  						OperationType: operation.OperationTypeCreate,
   218  						CreationTime:  now,
   219  					},
   220  					Status: operation.OperationStatusSucceeded,
   221  				},
   222  			},
   223  			{
   224  				Name:        "Successful UPDATE Operation",
   225  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: timeToTimePtr(now.Add(1 * time.Minute)), DeletedAt: &time.Time{}, Ready: true}},
   226  				ExpectedResponse: operation.OperationResponse{
   227  					Operation: &operation.Operation{
   228  						ResourceID:    resourceID,
   229  						ResourceType:  resource.Application,
   230  						OperationType: operation.OperationTypeUpdate,
   231  						CreationTime:  now.Add(1 * time.Minute),
   232  					},
   233  					Status: operation.OperationStatusSucceeded,
   234  				},
   235  			},
   236  			{
   237  				Name:        "Successful DELETE Operation",
   238  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &now, DeletedAt: timeToTimePtr(now.Add(1 * time.Minute)), Ready: true}},
   239  				ExpectedResponse: operation.OperationResponse{
   240  					Operation: &operation.Operation{
   241  						ResourceID:    resourceID,
   242  						ResourceType:  resource.Application,
   243  						OperationType: operation.OperationTypeDelete,
   244  						CreationTime:  now.Add(1 * time.Minute),
   245  					},
   246  					Status: operation.OperationStatusSucceeded,
   247  				},
   248  			},
   249  			{
   250  				Name:        "In Progress CREATE Operation",
   251  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &time.Time{}, DeletedAt: &time.Time{}, Ready: false}},
   252  				ExpectedResponse: operation.OperationResponse{
   253  					Operation: &operation.Operation{
   254  						ResourceID:    resourceID,
   255  						ResourceType:  resource.Application,
   256  						OperationType: operation.OperationTypeCreate,
   257  						CreationTime:  now,
   258  					},
   259  					Status: operation.OperationStatusInProgress,
   260  				},
   261  			},
   262  			{
   263  				Name:        "In Progress UPDATE Operation",
   264  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: timeToTimePtr(now.Add(1 * time.Minute)), DeletedAt: &time.Time{}, Ready: false}},
   265  				ExpectedResponse: operation.OperationResponse{
   266  					Operation: &operation.Operation{
   267  						ResourceID:    resourceID,
   268  						ResourceType:  resource.Application,
   269  						OperationType: operation.OperationTypeUpdate,
   270  						CreationTime:  now.Add(1 * time.Minute),
   271  					},
   272  					Status: operation.OperationStatusInProgress,
   273  				},
   274  			},
   275  			{
   276  				Name:        "In Progress DELETE Operation",
   277  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &now, DeletedAt: timeToTimePtr(now.Add(1 * time.Minute)), Ready: false}},
   278  				ExpectedResponse: operation.OperationResponse{
   279  					Operation: &operation.Operation{
   280  						ResourceID:    resourceID,
   281  						ResourceType:  resource.Application,
   282  						OperationType: operation.OperationTypeDelete,
   283  						CreationTime:  now.Add(1 * time.Minute),
   284  					},
   285  					Status: operation.OperationStatusInProgress,
   286  				},
   287  			},
   288  			{
   289  				Name:        "Failed CREATE Operation",
   290  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &time.Time{}, DeletedAt: &time.Time{}, Ready: true, Error: &mockedErr}},
   291  				ExpectedResponse: operation.OperationResponse{
   292  					Operation: &operation.Operation{
   293  						ResourceID:    resourceID,
   294  						ResourceType:  resource.Application,
   295  						OperationType: operation.OperationTypeCreate,
   296  						CreationTime:  now,
   297  					},
   298  					Status: operation.OperationStatusFailed,
   299  					Error:  &mockedErr,
   300  				},
   301  			},
   302  			{
   303  				Name:        "Failed UPDATE Operation",
   304  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: timeToTimePtr(now.Add(1 * time.Minute)), DeletedAt: &time.Time{}, Ready: true, Error: &mockedErr}},
   305  				ExpectedResponse: operation.OperationResponse{
   306  					Operation: &operation.Operation{
   307  						ResourceID:    resourceID,
   308  						ResourceType:  resource.Application,
   309  						OperationType: operation.OperationTypeUpdate,
   310  						CreationTime:  now.Add(1 * time.Minute),
   311  					},
   312  					Status: operation.OperationStatusFailed,
   313  					Error:  &mockedErr,
   314  				},
   315  			},
   316  			{
   317  				Name:        "Failed DELETE Operation",
   318  				Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &now, DeletedAt: timeToTimePtr(now.Add(1 * time.Minute)), Ready: true, Error: &mockedErr}},
   319  				ExpectedResponse: operation.OperationResponse{
   320  					Operation: &operation.Operation{
   321  						ResourceID:    resourceID,
   322  						ResourceType:  resource.Application,
   323  						OperationType: operation.OperationTypeDelete,
   324  						CreationTime:  now.Add(1 * time.Minute),
   325  					},
   326  					Status: operation.OperationStatusFailed,
   327  					Error:  &mockedErr,
   328  				},
   329  			},
   330  		}
   331  
   332  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceedsMultipleTimes(len(cases))
   333  		defer mockedTx.AssertExpectations(t)
   334  		defer mockedTransactioner.AssertExpectations(t)
   335  
   336  		for _, testCase := range cases {
   337  			t.Run(testCase.Name, func(t *testing.T) {
   338  				handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) {
   339  					return testCase.Application, nil
   340  				}, loadTenantFunc)
   341  
   342  				writer := httptest.NewRecorder()
   343  				handler.ServeHTTP(writer, req)
   344  
   345  				expectedBody, err := json.Marshal(testCase.ExpectedResponse)
   346  				require.NoError(t, err)
   347  
   348  				require.Equal(t, http.StatusOK, writer.Code)
   349  				require.Equal(t, string(expectedBody), strings.TrimSpace(writer.Body.String()))
   350  			})
   351  		}
   352  	})
   353  }
   354  
   355  func fixEmptyRequest(t *testing.T, ctx context.Context, resourceType string, resourceID string) *http.Request {
   356  	endpointPath := path.Join("/", resourceType, resourceID)
   357  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointPath, nil)
   358  	vars := map[string]string{"resource_type": resourceType, "resource_id": resourceID}
   359  	req = mux.SetURLVars(req, vars)
   360  	require.NoError(t, err)
   361  
   362  	return req
   363  }
   364  
   365  func loadTenantFunc(_ context.Context) (string, error) {
   366  	return tenantID, nil
   367  }
   368  
   369  func timeToTimePtr(time time.Time) *time.Time {
   370  	return &time
   371  }