diff --git a/resources/postman/Switcher GitOps.postman_collection.json b/resources/postman/Switcher GitOps.postman_collection.json index c8cd7fd..e2a59c1 100644 --- a/resources/postman/Switcher GitOps.postman_collection.json +++ b/resources/postman/Switcher GitOps.postman_collection.json @@ -16,7 +16,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n\t\"repository\": \"{{github_url}}\",\r\n\t\"token\": \"{{github_pat}}\",\r\n\t\"branch\": \"main\",\r\n \"environment\": \"default\",\r\n\t\"domain\": {\r\n\t\t\"id\": \"{{domain_id}}\",\r\n\t\t\"name\": \"GitOps\"\r\n\t},\r\n\t\"settings\": {\r\n\t\t\"active\": true,\r\n\t\t\"window\": \"30s\",\r\n\t\t\"forceprune\": false\r\n\t}\t\r\n}", + "raw": "{\r\n\t\"repository\": \"{{github_url}}\",\r\n\t\"token\": \"{{github_pat}}\",\r\n\t\"branch\": \"{{github_branch}}\",\r\n \"environment\": \"{{environment}}\",\r\n\t\"domain\": {\r\n\t\t\"id\": \"{{domain_id}}\",\r\n\t\t\"name\": \"GitOps\"\r\n\t},\r\n\t\"settings\": {\r\n\t\t\"active\": true,\r\n\t\t\"window\": \"30s\",\r\n\t\t\"forceprune\": true\r\n\t}\t\r\n}", "options": { "raw": { "language": "json" @@ -42,7 +42,59 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n\t\"repository\": \"{{github_url}}\",\r\n \"branch\": \"main\",\r\n \"environment\": \"default\",\r\n \"domain\": {\r\n\t\t\"id\": \"{{domain_id}}\",\r\n \"name\": \"GitOps\"\r\n },\r\n \"settings\": {\r\n \"active\": true,\r\n \"window\": \"30s\",\r\n \"forceprune\": false\r\n }\r\n}", + "raw": "{\r\n\t\"repository\": \"{{github_url}}\",\r\n\t\"branch\": \"{{github_branch}}\",\r\n \"environment\": \"{{environment}}\",\r\n \"domain\": {\r\n\t\t\"id\": \"{{domain_id}}\",\r\n \"name\": \"GitOps\"\r\n },\r\n \"settings\": {\r\n \"active\": true,\r\n \"window\": \"30s\",\r\n \"forceprune\": true\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/account", + "host": [ + "{{url}}" + ], + "path": [ + "account" + ] + } + }, + "response": [] + }, + { + "name": "Update (token)", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"repository\": \"{{github_url}}\",\r\n \"token\": \"{{github_pat}}\",\r\n\t\"branch\": \"{{github_branch}}\",\r\n \"environment\": \"{{environment}}\",\r\n \"domain\": {\r\n\t\t\"id\": \"{{domain_id}}\",\r\n \"name\": \"GitOps\"\r\n },\r\n \"settings\": {\r\n \"active\": true,\r\n \"window\": \"30s\",\r\n \"forceprune\": true\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/account", + "host": [ + "{{url}}" + ], + "path": [ + "account" + ] + } + }, + "response": [] + }, + { + "name": "Update (force sync)", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"repository\": \"{{github_url}}\",\r\n\t\"branch\": \"{{github_branch}}\",\r\n \"environment\": \"{{environment}}\",\r\n \"domain\": {\r\n\t\t\"id\": \"{{domain_id}}\",\r\n \"name\": \"GitOps\",\r\n \"lastcommit\": \"refresh\"\r\n },\r\n \"settings\": {\r\n \"active\": true,\r\n \"window\": \"30s\",\r\n \"forceprune\": true\r\n }\r\n}", "options": { "raw": { "language": "json" @@ -97,14 +149,14 @@ "method": "GET", "header": [], "url": { - "raw": "{{url}}/account/{{domain_id}}/default", + "raw": "{{url}}/account/{{domain_id}}/{{environment}}", "host": [ "{{url}}" ], "path": [ "account", "{{domain_id}}", - "default" + "{{environment}}" ] } }, @@ -125,14 +177,14 @@ } }, "url": { - "raw": "{{url}}/account/{{domain_id}}/default", + "raw": "{{url}}/account/{{domain_id}}/{{environment}}", "host": [ "{{url}}" ], "path": [ "account", "{{domain_id}}", - "default" + "{{environment}}" ] } }, diff --git a/src/core/api.go b/src/core/api.go index c7c11f9..bc8b8fa 100644 --- a/src/core/api.go +++ b/src/core/api.go @@ -3,6 +3,7 @@ package core import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -19,6 +20,7 @@ type GraphQLRequest struct { type PushChangeResponse struct { Message string `json:"message"` + Error string `json:"error"` Version int `json:"version"` } @@ -71,7 +73,7 @@ func (a *ApiService) FetchSnapshot(domainId string, environment string) (string, func (a *ApiService) PushChanges(domainId string, diff model.DiffResult) (PushChangeResponse, error) { reqBody, _ := json.Marshal(diff) - responseBody, err := a.doPostRequest(a.apiUrl+config.GetEnv("SWITCHER_PATH_PUSH"), domainId, reqBody) + responseBody, status, err := a.doPostRequest(a.apiUrl+config.GetEnv("SWITCHER_PATH_PUSH"), domainId, reqBody) if err != nil { return PushChangeResponse{}, err @@ -79,6 +81,11 @@ func (a *ApiService) PushChanges(domainId string, diff model.DiffResult) (PushCh var response PushChangeResponse json.Unmarshal([]byte(responseBody), &response) + + if status != http.StatusOK { + return PushChangeResponse{}, errors.New(response.Error) + } + return response, nil } @@ -105,7 +112,7 @@ func (a *ApiService) doGraphQLRequest(domainId string, query string) (string, er return string(responseBody), nil } -func (a *ApiService) doPostRequest(url string, domainId string, body []byte) (string, error) { +func (a *ApiService) doPostRequest(url string, domainId string, body []byte) (string, int, error) { // Generate a bearer token token := generateBearerToken(a.apiKey, domainId) @@ -119,12 +126,12 @@ func (a *ApiService) doPostRequest(url string, domainId string, body []byte) (st client := &http.Client{} resp, err := client.Do(req) if err != nil { - return "", err + return "", 0, err } defer resp.Body.Close() responseBody, _ := io.ReadAll(resp.Body) - return string(responseBody), nil + return string(responseBody), resp.StatusCode, nil } func generateBearerToken(apiKey string, subject string) string { diff --git a/src/core/api_test.go b/src/core/api_test.go index 9d59fdc..0664d68 100644 --- a/src/core/api_test.go +++ b/src/core/api_test.go @@ -134,20 +134,37 @@ func TestPushChangesToAPI(t *testing.T) { assert.Equal(t, "Changes applied successfully", response.Message) }) - t.Run("Should return error - invalid API key", func(t *testing.T) { + t.Run("Should return error - invalid payload (400)", func(t *testing.T) { + // Given + diff := givenDiffResult("default") + fakeApiServer := givenApiResponse(http.StatusBadRequest, `{ "error": "Config already exists" }`) + defer fakeApiServer.Close() + + apiService := NewApiService(SWITCHER_API_JWT_SECRET, fakeApiServer.URL) + + // Test + _, err := apiService.PushChanges("domainId", diff) + + // Assert + assert.NotNil(t, err) + assert.Equal(t, "Config already exists", err.Error()) + + }) + + t.Run("Should return error - invalid API key (401)", func(t *testing.T) { // Given diff := givenDiffResult("default") - fakeApiServer := givenApiResponse(http.StatusUnauthorized, `{ "message": "Invalid API token" }`) + fakeApiServer := givenApiResponse(http.StatusUnauthorized, `{ "error": "Invalid API token" }`) defer fakeApiServer.Close() apiService := NewApiService("[INVALID_KEY]", fakeApiServer.URL) // Test - response, _ := apiService.PushChanges("domainId", diff) + _, err := apiService.PushChanges("domainId", diff) // Assert - assert.NotNil(t, response) - assert.Contains(t, response.Message, "Invalid API token") + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "Invalid API token") }) t.Run("Should return error - API not accessible", func(t *testing.T) { diff --git a/src/core/handler.go b/src/core/handler.go index 2e80eaa..2006dde 100644 --- a/src/core/handler.go +++ b/src/core/handler.go @@ -97,6 +97,12 @@ func (c *CoreHandler) StartAccountHandler(accountId string, gitService IGitServi continue } + if !utils.IsJsonValid(repositoryData.Content, &model.Snapshot{}) { + c.updateDomainStatus(*account, model.StatusError, "Invalid JSON content", utils.LogLevelError) + time.Sleep(time.Duration(c.waitingTime)) + continue + } + // Fetch snapshot version from API snapshotVersionPayload, err := c.apiService.FetchSnapshotVersion(account.Domain.ID, account.Environment) @@ -249,7 +255,8 @@ func (c *CoreHandler) isOutSync(account model.Account, lastCommit string, snapsh return account.Domain.LastCommit == "" || // First sync account.Domain.LastCommit != lastCommit || // Repository out of sync - account.Domain.Version != snapshotVersion // API out of sync + account.Domain.Version != snapshotVersion || // API out of sync + account.Domain.Status != model.StatusSynced // Account out of sync } func (c *CoreHandler) isRepositoryOutSync(repositoryData *model.RepositoryData, diff model.DiffResult) bool { diff --git a/src/core/handler_test.go b/src/core/handler_test.go index 30b243f..cfa67f3 100644 --- a/src/core/handler_test.go +++ b/src/core/handler_test.go @@ -90,6 +90,41 @@ func TestAccountHandlerSyncRepository(t *testing.T) { tearDown() }) + t.Run("Should sync successfully after account reactivation when it was pending", func(t *testing.T) { + // Given + fakeGitService := NewFakeGitService() + fakeApiService := NewFakeApiService() + fakeApiService.pushChanges = PushChangeResponse{ + Message: "Changes applied successfully", + Version: 1, + } + coreHandler = NewCoreHandler(coreHandler.accountRepository, fakeApiService, NewComparatorService()) + + account := givenAccount() + account.Domain.ID = "123-pending" + account.Domain.Status = model.StatusPending + account.Domain.Message = "Account was deactivated" + account.Domain.LastCommit = "123" + account.Domain.Version = 1 + accountCreated, _ := coreHandler.accountRepository.Create(&account) + + // Test + go coreHandler.StartAccountHandler(accountCreated.ID.Hex(), fakeGitService) + + // Wait for goroutine to process + time.Sleep(1 * time.Second) + + // Assert + accountFromDb, _ := coreHandler.accountRepository.FetchByAccountId(string(accountCreated.ID.Hex())) + assert.Equal(t, model.StatusSynced, accountFromDb.Domain.Status) + assert.Contains(t, accountFromDb.Domain.Message, model.MessageSynced) + assert.Equal(t, "123", accountFromDb.Domain.LastCommit) + assert.Equal(t, 1, accountFromDb.Domain.Version) + assert.NotEqual(t, "", accountFromDb.Domain.LastDate) + + tearDown() + }) + t.Run("Should sync successfully when repository is out of sync", func(t *testing.T) { // Given fakeGitService := NewFakeGitService() @@ -309,6 +344,35 @@ func TestAccountHandlerNotSync(t *testing.T) { tearDown() }) + t.Run("Should not sync when fetch repository data returns a malformed JSON content", func(t *testing.T) { + // Given + fakeGitService := NewFakeGitService() + fakeGitService.content = `{ + "domain": { + "group": [{ + "name": "Release 1", + "description": "Showcase configuration", + "activated": true + }` + + account := givenAccount() + account.Domain.ID = "123-error-malformed-json" + accountCreated, _ := coreHandler.accountRepository.Create(&account) + + // Test + go coreHandler.StartAccountHandler(accountCreated.ID.Hex(), fakeGitService) + + time.Sleep(1 * time.Second) + + // Assert + accountFromDb, _ := coreHandler.accountRepository.FetchByDomainIdEnvironment(accountCreated.Domain.ID, accountCreated.Environment) + assert.Equal(t, model.StatusError, accountFromDb.Domain.Status) + assert.Contains(t, accountFromDb.Domain.Message, "Invalid JSON content") + assert.Equal(t, "", accountFromDb.Domain.LastCommit) + + tearDown() + }) + t.Run("Should not sync when fetch snapshot version returns an error", func(t *testing.T) { // Given fakeGitService := NewFakeGitService() diff --git a/src/utils/util.go b/src/utils/util.go index 438d448..31dcf1c 100644 --- a/src/utils/util.go +++ b/src/utils/util.go @@ -35,6 +35,11 @@ func ReadJsonFromFile(path string) string { return string(bs) } +func IsJsonValid(jsonString string, obj interface{}) bool { + err := json.Unmarshal([]byte(jsonString), &obj) + return err == nil +} + func ToJsonFromObject(object interface{}) string { json, _ := json.MarshalIndent(object, "", " ") return string(json) diff --git a/src/utils/util_test.go b/src/utils/util_test.go index 4e74c55..8ed6453 100644 --- a/src/utils/util_test.go +++ b/src/utils/util_test.go @@ -28,15 +28,45 @@ func TestToMapFromObject(t *testing.T) { } func TestFormatJSON(t *testing.T) { - account := givenAccount(true) - accountJSON := ToJsonFromObject(account) - actual := FormatJSON(accountJSON) - assert.NotNil(t, actual) + t.Run("valid", func(t *testing.T) { + account := givenAccount(true) + accountJSON := ToJsonFromObject(account) + actual := FormatJSON(accountJSON) + assert.NotNil(t, actual) + }) + + t.Run("invalid", func(t *testing.T) { + actual := FormatJSON("invalid") + assert.NotNil(t, actual) + }) } -func TestFormatJSONError(t *testing.T) { - actual := FormatJSON("invalid") - assert.NotNil(t, actual) +func TestIsValidJson(t *testing.T) { + t.Run("valid", func(t *testing.T) { + invalidJson := `{ + "domain": { + "group": [{ + "name": "Hi There", + "activated": true + }] + } + }` + + assert.True(t, IsJsonValid(invalidJson, model.Snapshot{})) + }) + + t.Run("invalid", func(t *testing.T) { + invalidJson := `{ + "domain": { + "group": [{ + "name": "Hi There", + "activated": true + }] + } + ` + + assert.False(t, IsJsonValid(invalidJson, model.Snapshot{})) + }) } func TestReadJsonFileToObject(t *testing.T) {