Skip to content

Commit 4cfd8f4

Browse files
authored
Fixes repo content validation and account syncing evaluation (#43)
1 parent 506b836 commit 4cfd8f4

File tree

7 files changed

+205
-23
lines changed

7 files changed

+205
-23
lines changed

resources/postman/Switcher GitOps.postman_collection.json

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"header": [],
1717
"body": {
1818
"mode": "raw",
19-
"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}",
19+
"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}",
2020
"options": {
2121
"raw": {
2222
"language": "json"
@@ -42,7 +42,59 @@
4242
"header": [],
4343
"body": {
4444
"mode": "raw",
45-
"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}",
45+
"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}",
46+
"options": {
47+
"raw": {
48+
"language": "json"
49+
}
50+
}
51+
},
52+
"url": {
53+
"raw": "{{url}}/account",
54+
"host": [
55+
"{{url}}"
56+
],
57+
"path": [
58+
"account"
59+
]
60+
}
61+
},
62+
"response": []
63+
},
64+
{
65+
"name": "Update (token)",
66+
"request": {
67+
"method": "PUT",
68+
"header": [],
69+
"body": {
70+
"mode": "raw",
71+
"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}",
72+
"options": {
73+
"raw": {
74+
"language": "json"
75+
}
76+
}
77+
},
78+
"url": {
79+
"raw": "{{url}}/account",
80+
"host": [
81+
"{{url}}"
82+
],
83+
"path": [
84+
"account"
85+
]
86+
}
87+
},
88+
"response": []
89+
},
90+
{
91+
"name": "Update (force sync)",
92+
"request": {
93+
"method": "PUT",
94+
"header": [],
95+
"body": {
96+
"mode": "raw",
97+
"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}",
4698
"options": {
4799
"raw": {
48100
"language": "json"
@@ -97,14 +149,14 @@
97149
"method": "GET",
98150
"header": [],
99151
"url": {
100-
"raw": "{{url}}/account/{{domain_id}}/default",
152+
"raw": "{{url}}/account/{{domain_id}}/{{environment}}",
101153
"host": [
102154
"{{url}}"
103155
],
104156
"path": [
105157
"account",
106158
"{{domain_id}}",
107-
"default"
159+
"{{environment}}"
108160
]
109161
}
110162
},
@@ -125,14 +177,14 @@
125177
}
126178
},
127179
"url": {
128-
"raw": "{{url}}/account/{{domain_id}}/default",
180+
"raw": "{{url}}/account/{{domain_id}}/{{environment}}",
129181
"host": [
130182
"{{url}}"
131183
],
132184
"path": [
133185
"account",
134186
"{{domain_id}}",
135-
"default"
187+
"{{environment}}"
136188
]
137189
}
138190
},

src/core/api.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package core
33
import (
44
"bytes"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"net/http"
@@ -19,6 +20,7 @@ type GraphQLRequest struct {
1920

2021
type PushChangeResponse struct {
2122
Message string `json:"message"`
23+
Error string `json:"error"`
2224
Version int `json:"version"`
2325
}
2426

@@ -71,14 +73,19 @@ func (a *ApiService) FetchSnapshot(domainId string, environment string) (string,
7173

7274
func (a *ApiService) PushChanges(domainId string, diff model.DiffResult) (PushChangeResponse, error) {
7375
reqBody, _ := json.Marshal(diff)
74-
responseBody, err := a.doPostRequest(a.apiUrl+config.GetEnv("SWITCHER_PATH_PUSH"), domainId, reqBody)
76+
responseBody, status, err := a.doPostRequest(a.apiUrl+config.GetEnv("SWITCHER_PATH_PUSH"), domainId, reqBody)
7577

7678
if err != nil {
7779
return PushChangeResponse{}, err
7880
}
7981

8082
var response PushChangeResponse
8183
json.Unmarshal([]byte(responseBody), &response)
84+
85+
if status != http.StatusOK {
86+
return PushChangeResponse{}, errors.New(response.Error)
87+
}
88+
8289
return response, nil
8390
}
8491

@@ -105,7 +112,7 @@ func (a *ApiService) doGraphQLRequest(domainId string, query string) (string, er
105112
return string(responseBody), nil
106113
}
107114

108-
func (a *ApiService) doPostRequest(url string, domainId string, body []byte) (string, error) {
115+
func (a *ApiService) doPostRequest(url string, domainId string, body []byte) (string, int, error) {
109116
// Generate a bearer token
110117
token := generateBearerToken(a.apiKey, domainId)
111118

@@ -119,12 +126,12 @@ func (a *ApiService) doPostRequest(url string, domainId string, body []byte) (st
119126
client := &http.Client{}
120127
resp, err := client.Do(req)
121128
if err != nil {
122-
return "", err
129+
return "", 0, err
123130
}
124131
defer resp.Body.Close()
125132

126133
responseBody, _ := io.ReadAll(resp.Body)
127-
return string(responseBody), nil
134+
return string(responseBody), resp.StatusCode, nil
128135
}
129136

130137
func generateBearerToken(apiKey string, subject string) string {

src/core/api_test.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,20 +134,37 @@ func TestPushChangesToAPI(t *testing.T) {
134134
assert.Equal(t, "Changes applied successfully", response.Message)
135135
})
136136

137-
t.Run("Should return error - invalid API key", func(t *testing.T) {
137+
t.Run("Should return error - invalid payload (400)", func(t *testing.T) {
138+
// Given
139+
diff := givenDiffResult("default")
140+
fakeApiServer := givenApiResponse(http.StatusBadRequest, `{ "error": "Config already exists" }`)
141+
defer fakeApiServer.Close()
142+
143+
apiService := NewApiService(SWITCHER_API_JWT_SECRET, fakeApiServer.URL)
144+
145+
// Test
146+
_, err := apiService.PushChanges("domainId", diff)
147+
148+
// Assert
149+
assert.NotNil(t, err)
150+
assert.Equal(t, "Config already exists", err.Error())
151+
152+
})
153+
154+
t.Run("Should return error - invalid API key (401)", func(t *testing.T) {
138155
// Given
139156
diff := givenDiffResult("default")
140-
fakeApiServer := givenApiResponse(http.StatusUnauthorized, `{ "message": "Invalid API token" }`)
157+
fakeApiServer := givenApiResponse(http.StatusUnauthorized, `{ "error": "Invalid API token" }`)
141158
defer fakeApiServer.Close()
142159

143160
apiService := NewApiService("[INVALID_KEY]", fakeApiServer.URL)
144161

145162
// Test
146-
response, _ := apiService.PushChanges("domainId", diff)
163+
_, err := apiService.PushChanges("domainId", diff)
147164

148165
// Assert
149-
assert.NotNil(t, response)
150-
assert.Contains(t, response.Message, "Invalid API token")
166+
assert.NotNil(t, err)
167+
assert.Contains(t, err.Error(), "Invalid API token")
151168
})
152169

153170
t.Run("Should return error - API not accessible", func(t *testing.T) {

src/core/handler.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ func (c *CoreHandler) StartAccountHandler(accountId string, gitService IGitServi
9797
continue
9898
}
9999

100+
if !utils.IsJsonValid(repositoryData.Content, &model.Snapshot{}) {
101+
c.updateDomainStatus(*account, model.StatusError, "Invalid JSON content", utils.LogLevelError)
102+
time.Sleep(time.Duration(c.waitingTime))
103+
continue
104+
}
105+
100106
// Fetch snapshot version from API
101107
snapshotVersionPayload, err := c.apiService.FetchSnapshotVersion(account.Domain.ID, account.Environment)
102108

@@ -249,7 +255,8 @@ func (c *CoreHandler) isOutSync(account model.Account, lastCommit string, snapsh
249255

250256
return account.Domain.LastCommit == "" || // First sync
251257
account.Domain.LastCommit != lastCommit || // Repository out of sync
252-
account.Domain.Version != snapshotVersion // API out of sync
258+
account.Domain.Version != snapshotVersion || // API out of sync
259+
account.Domain.Status != model.StatusSynced // Account out of sync
253260
}
254261

255262
func (c *CoreHandler) isRepositoryOutSync(repositoryData *model.RepositoryData, diff model.DiffResult) bool {

src/core/handler_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,41 @@ func TestAccountHandlerSyncRepository(t *testing.T) {
9090
tearDown()
9191
})
9292

93+
t.Run("Should sync successfully after account reactivation when it was pending", func(t *testing.T) {
94+
// Given
95+
fakeGitService := NewFakeGitService()
96+
fakeApiService := NewFakeApiService()
97+
fakeApiService.pushChanges = PushChangeResponse{
98+
Message: "Changes applied successfully",
99+
Version: 1,
100+
}
101+
coreHandler = NewCoreHandler(coreHandler.accountRepository, fakeApiService, NewComparatorService())
102+
103+
account := givenAccount()
104+
account.Domain.ID = "123-pending"
105+
account.Domain.Status = model.StatusPending
106+
account.Domain.Message = "Account was deactivated"
107+
account.Domain.LastCommit = "123"
108+
account.Domain.Version = 1
109+
accountCreated, _ := coreHandler.accountRepository.Create(&account)
110+
111+
// Test
112+
go coreHandler.StartAccountHandler(accountCreated.ID.Hex(), fakeGitService)
113+
114+
// Wait for goroutine to process
115+
time.Sleep(1 * time.Second)
116+
117+
// Assert
118+
accountFromDb, _ := coreHandler.accountRepository.FetchByAccountId(string(accountCreated.ID.Hex()))
119+
assert.Equal(t, model.StatusSynced, accountFromDb.Domain.Status)
120+
assert.Contains(t, accountFromDb.Domain.Message, model.MessageSynced)
121+
assert.Equal(t, "123", accountFromDb.Domain.LastCommit)
122+
assert.Equal(t, 1, accountFromDb.Domain.Version)
123+
assert.NotEqual(t, "", accountFromDb.Domain.LastDate)
124+
125+
tearDown()
126+
})
127+
93128
t.Run("Should sync successfully when repository is out of sync", func(t *testing.T) {
94129
// Given
95130
fakeGitService := NewFakeGitService()
@@ -309,6 +344,35 @@ func TestAccountHandlerNotSync(t *testing.T) {
309344
tearDown()
310345
})
311346

347+
t.Run("Should not sync when fetch repository data returns a malformed JSON content", func(t *testing.T) {
348+
// Given
349+
fakeGitService := NewFakeGitService()
350+
fakeGitService.content = `{
351+
"domain": {
352+
"group": [{
353+
"name": "Release 1",
354+
"description": "Showcase configuration",
355+
"activated": true
356+
}`
357+
358+
account := givenAccount()
359+
account.Domain.ID = "123-error-malformed-json"
360+
accountCreated, _ := coreHandler.accountRepository.Create(&account)
361+
362+
// Test
363+
go coreHandler.StartAccountHandler(accountCreated.ID.Hex(), fakeGitService)
364+
365+
time.Sleep(1 * time.Second)
366+
367+
// Assert
368+
accountFromDb, _ := coreHandler.accountRepository.FetchByDomainIdEnvironment(accountCreated.Domain.ID, accountCreated.Environment)
369+
assert.Equal(t, model.StatusError, accountFromDb.Domain.Status)
370+
assert.Contains(t, accountFromDb.Domain.Message, "Invalid JSON content")
371+
assert.Equal(t, "", accountFromDb.Domain.LastCommit)
372+
373+
tearDown()
374+
})
375+
312376
t.Run("Should not sync when fetch snapshot version returns an error", func(t *testing.T) {
313377
// Given
314378
fakeGitService := NewFakeGitService()

src/utils/util.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ func ReadJsonFromFile(path string) string {
3535
return string(bs)
3636
}
3737

38+
func IsJsonValid(jsonString string, obj interface{}) bool {
39+
err := json.Unmarshal([]byte(jsonString), &obj)
40+
return err == nil
41+
}
42+
3843
func ToJsonFromObject(object interface{}) string {
3944
json, _ := json.MarshalIndent(object, "", " ")
4045
return string(json)

src/utils/util_test.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,45 @@ func TestToMapFromObject(t *testing.T) {
2828
}
2929

3030
func TestFormatJSON(t *testing.T) {
31-
account := givenAccount(true)
32-
accountJSON := ToJsonFromObject(account)
33-
actual := FormatJSON(accountJSON)
34-
assert.NotNil(t, actual)
31+
t.Run("valid", func(t *testing.T) {
32+
account := givenAccount(true)
33+
accountJSON := ToJsonFromObject(account)
34+
actual := FormatJSON(accountJSON)
35+
assert.NotNil(t, actual)
36+
})
37+
38+
t.Run("invalid", func(t *testing.T) {
39+
actual := FormatJSON("invalid")
40+
assert.NotNil(t, actual)
41+
})
3542
}
3643

37-
func TestFormatJSONError(t *testing.T) {
38-
actual := FormatJSON("invalid")
39-
assert.NotNil(t, actual)
44+
func TestIsValidJson(t *testing.T) {
45+
t.Run("valid", func(t *testing.T) {
46+
invalidJson := `{
47+
"domain": {
48+
"group": [{
49+
"name": "Hi There",
50+
"activated": true
51+
}]
52+
}
53+
}`
54+
55+
assert.True(t, IsJsonValid(invalidJson, model.Snapshot{}))
56+
})
57+
58+
t.Run("invalid", func(t *testing.T) {
59+
invalidJson := `{
60+
"domain": {
61+
"group": [{
62+
"name": "Hi There",
63+
"activated": true
64+
}]
65+
}
66+
`
67+
68+
assert.False(t, IsJsonValid(invalidJson, model.Snapshot{}))
69+
})
4070
}
4171

4272
func TestReadJsonFileToObject(t *testing.T) {

0 commit comments

Comments
 (0)