From b6ce11bc3615e22e1581e920ee6df5a4612f3a07 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:33:33 -0700 Subject: [PATCH] GitOps Account - added account update APIs --- npm-shrinkwrap.json | 20 +- package.json | 2 +- requests/Switcher API.postman_collection.json | 116 ++++++++ src/api-docs/paths/path-gitops.js | 69 +++++ src/api-docs/schemas/gitops.js | 57 ++++ src/external/gitops.js | 14 + src/routers/gitops.js | 37 +++ src/services/gitops/index.js | 24 ++ tests/gitops-account.test.js | 277 +++++++++++++++++- 9 files changed, 604 insertions(+), 12 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 64d83c6..af2b301 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -23,7 +23,7 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "mongodb": "^6.9.0", - "mongoose": "^8.7.0", + "mongoose": "^8.7.1", "pino": "^9.4.0", "pino-pretty": "^11.2.2", "swagger-ui-express": "^5.0.1", @@ -2366,9 +2366,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.33", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz", - "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==", + "version": "1.5.35", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.35.tgz", + "integrity": "sha512-hOSRInrIDm0Brzp4IHW2F/VM+638qOL2CzE0DgpnGzKW27C95IqqeqgKz/hxHGnvPxvQGpHUGD5qRVC9EZY2+A==", "dev": true }, "node_modules/emittery": { @@ -2966,9 +2966,9 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -4690,9 +4690,9 @@ } }, "node_modules/mongoose": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.0.tgz", - "integrity": "sha512-rUCSF1mMYQXjXYdqEQLLlMD3xbcj2j1/hRn+9VnVj7ipzru/UoUZxlj/hWmteKMAh4EFnDZ+BIrmma9l/0Hi1g==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.1.tgz", + "integrity": "sha512-RpNMyhyzLVCVbf8xTVbrf/18G3MqQzNw5pJdvOJ60fzbCa3cOZzz9L+8XpqzBXtRlgZGWv0T7MmOtvrT8ocp1Q==", "dependencies": { "bson": "^6.7.0", "kareem": "2.6.3", diff --git a/package.json b/package.json index d78c5a2..b25c93b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "mongodb": "^6.9.0", - "mongoose": "^8.7.0", + "mongoose": "^8.7.1", "pino": "^9.4.0", "pino-pretty": "^11.2.2", "swagger-ui-express": "^5.0.1", diff --git a/requests/Switcher API.postman_collection.json b/requests/Switcher API.postman_collection.json index 0f15eff..8760449 100644 --- a/requests/Switcher API.postman_collection.json +++ b/requests/Switcher API.postman_collection.json @@ -5678,6 +5678,122 @@ } }, "response": [] + }, + { + "name": "GitOps Account - Update", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{authToken}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"repository\": \"{{gitops_github_url}}\",\r\n\t\"branch\": \"staging\",\r\n \"environment\": \"default\",\r\n\t\"domain\": {\r\n\t\t\"id\": \"{{gitops_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" + } + } + }, + "url": { + "raw": "{{url}}/gitops/v1/account", + "host": [ + "{{url}}" + ], + "path": [ + "gitops", + "v1", + "account" + ] + } + }, + "response": [] + }, + { + "name": "GitOps Account - Update Token", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{authToken}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"environment\": \"default\",\r\n \"token\": \"{{gitops_github_pat}}\",\r\n\t\"domain\": {\r\n\t\t\"id\": \"{{gitops_domain_id}}\"\r\n\t}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/gitops/v1/account/token", + "host": [ + "{{url}}" + ], + "path": [ + "gitops", + "v1", + "account", + "token" + ] + } + }, + "response": [] + }, + { + "name": "GitOps Account - Force sync", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{authToken}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"environment\": \"default\",\r\n\t\"domain\": {\r\n\t\t\"id\": \"{{gitops_domain_id}}\"\r\n\t}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/gitops/v1/account/forcesync", + "host": [ + "{{url}}" + ], + "path": [ + "gitops", + "v1", + "account", + "forcesync" + ] + } + }, + "response": [] } ] } diff --git a/src/api-docs/paths/path-gitops.js b/src/api-docs/paths/path-gitops.js index c0d2322..3ddb57f 100644 --- a/src/api-docs/paths/path-gitops.js +++ b/src/api-docs/paths/path-gitops.js @@ -72,5 +72,74 @@ export default { } } } + }, + '/gitops/v1/account': { + put: { + tags: ['Switcher GitOps'], + description: 'Update an account to receive gitops changes', + requestBody: { + content: commonSchemaContent('GitOpsAccountUpdateRequest') + }, + responses: { + 200: { + description: 'Account update successful', + content: commonSchemaContent('GitOpsAccountResponse') + }, + 400: { + description: 'Invalid request body', + content: commonSchemaContent('ErrorResponse') + }, + 500: { + description: 'Something went wrong while updating the account', + content: commonSchemaContent('ErrorResponse') + } + } + } + }, + '/gitops/v1/account/token': { + put: { + tags: ['Switcher GitOps'], + description: 'Update an account token to receive gitops changes', + requestBody: { + content: commonSchemaContent('GitOpsAccountTokenUpdateRequest') + }, + responses: { + 200: { + description: 'Account token update successful', + content: commonSchemaContent('GitOpsAccountResponse') + }, + 400: { + description: 'Invalid request body', + content: commonSchemaContent('ErrorResponse') + }, + 500: { + description: 'Something went wrong while updating the account token', + content: commonSchemaContent('ErrorResponse') + } + } + } + }, + '/gitops/v1/account/forcesync': { + put: { + tags: ['Switcher GitOps'], + description: 'Force sync an account to receive gitops changes', + requestBody: { + content: commonSchemaContent('GitOpsAccountForceSyncRequest') + }, + responses: { + 200: { + description: 'Account force sync successful', + content: commonSchemaContent('GitOpsAccountResponse') + }, + 400: { + description: 'Invalid request body', + content: commonSchemaContent('ErrorResponse') + }, + 500: { + description: 'Something went wrong while force syncing the account', + content: commonSchemaContent('ErrorResponse') + } + } + } } }; \ No newline at end of file diff --git a/src/api-docs/schemas/gitops.js b/src/api-docs/schemas/gitops.js index d1ea787..5df6801 100644 --- a/src/api-docs/schemas/gitops.js +++ b/src/api-docs/schemas/gitops.js @@ -74,6 +74,29 @@ const accountRequest = { } }; +const accountUpdateRequest = { + repository: { + type: 'string', + description: 'The repository URL' + }, + branch: { + type: 'string', + description: 'The branch name' + }, + environment: { + type: 'string', + description: 'The environment name' + }, + domain: { + type: 'object', + properties: domain + }, + settings: { + type: 'object', + properties: settings + } +}; + const accountResponse = { _id: { type: 'string', @@ -150,6 +173,40 @@ export default { type: 'object', properties: accountRequest }, + GitOpsAccountUpdateRequest: { + type: 'object', + properties: accountUpdateRequest + }, + GitOpsAccountTokenUpdateRequest: { + type: 'object', + properties: { + token: { + type: 'string', + description: 'The Git token' + }, + environment: { + type: 'string', + description: 'The environment name' + }, + domain: { + type: 'object', + properties: domain + } + } + }, + GitOpsAccountForceSyncRequest: { + type: 'object', + properties: { + environment: { + type: 'string', + description: 'The environment name' + }, + domain: { + type: 'object', + properties: domain + } + } + }, GitOpsAccountResponse: { type: 'object', properties: accountResponse diff --git a/src/external/gitops.js b/src/external/gitops.js index 508d975..5592667 100644 --- a/src/external/gitops.js +++ b/src/external/gitops.js @@ -26,6 +26,20 @@ export async function createAccount(account) { return response.data; } +export async function updateAccount(account) { + const url = `${process.env.SWITCHER_GITOPS_URL}/account`; + const response = await axios.put(url, account, { + httpsAgent: agent(url), + headers: headers(account.domain.id) + }); + + if (response.status !== 200) { + throw new GitOpsError(`Failed to update account [${response.status}] ${JSON.stringify(response.data)}`); + } + + return response.data; +} + function generateToken(subject) { const options = { expiresIn: '1m' diff --git a/src/routers/gitops.js b/src/routers/gitops.js index 95638a4..fc70098 100644 --- a/src/routers/gitops.js +++ b/src/routers/gitops.js @@ -62,4 +62,41 @@ router.post('/gitops/v1/account/subscribe', auth, accountValidators, validate, } }); +router.put('/gitops/v1/account', auth, [ + body('environment').isString(), + body('domain.id').isMongoId().withMessage('Invalid domain ID'), +], validate, featureFlag, async (req, res) => { + try { + const account = await Service.updateAccount(req.body); + res.status(200).send(account); + } catch (e) { + responseExceptionSilent(res, e, 500, 'Account update failed'); + } +}); + +router.put('/gitops/v1/account/token', auth, [ + body('environment').isString(), + body('token').isString(), + body('domain.id').isMongoId().withMessage('Invalid domain ID'), +], validate, featureFlag, async (req, res) => { + try { + const account = await Service.updateAccountToken(req.body); + res.status(200).send(account); + } catch (e) { + responseExceptionSilent(res, e, 500, 'Account token update failed'); + } +}); + +router.put('/gitops/v1/account/forcesync', auth, [ + body('environment').isString(), + body('domain.id').isMongoId().withMessage('Invalid domain ID'), +], validate, featureFlag, async (req, res) => { + try { + const account = await Service.forceSyncAccount(req.body); + res.status(200).send(account); + } catch (e) { + responseExceptionSilent(res, e, 500, 'Account force sync failed'); + } +}); + export default router; \ No newline at end of file diff --git a/src/services/gitops/index.js b/src/services/gitops/index.js index 68f81e5..6bfa0cc 100644 --- a/src/services/gitops/index.js +++ b/src/services/gitops/index.js @@ -31,4 +31,28 @@ export async function pushChanges(domainId, environment, changes) { export async function subscribeAccount(account) { return GitOpsFacade.createAccount(account); +} + +export async function updateAccount(account) { + return GitOpsFacade.updateAccount(account); +} + +export async function updateAccountToken(account) { + return GitOpsFacade.updateAccount({ + environment: account.environment, + token: account.token, + domain: { + id: account.domain.id + } + }); +} + +export async function forceSyncAccount(account) { + return GitOpsFacade.updateAccount({ + environment: account.environment, + lastcommit: 'refresh', + domain: { + id: account.domain.id + } + }); } \ No newline at end of file diff --git a/tests/gitops-account.test.js b/tests/gitops-account.test.js index 7a43dc9..9b16e38 100644 --- a/tests/gitops-account.test.js +++ b/tests/gitops-account.test.js @@ -27,6 +27,37 @@ const VALID_SUBSCRIPTION_REQUEST = { } }; +const VALID_UPDATE_REQUEST = { + repository: 'https://github.com/switcherapi/switcher-gitops-fixture', + branch: 'main', + environment: EnvType.DEFAULT, + domain: { + id: String(domainId), + name: 'Test Domain' + }, + settings: { + active: true, + window: '30s', + forceprune: true + } +}; + +const VALID_TOKEN_UPDATE_REQUEST = { + environment: EnvType.DEFAULT, + token: '123456', + domain: { + id: String(domainId) + } +}; + +const VALID_FORCE_SYNC_REQUEST = { + environment: EnvType.DEFAULT, + domain: { + id: String(domainId) + } +}; + + afterAll(async () => { await new Promise(resolve => setTimeout(resolve, 1000)); await mongoose.disconnect(); @@ -172,4 +203,248 @@ describe('GitOps Account - Subscribe', () => { expect(req.body.errors[0].msg).toBe('Invalid window value'); }); -}); \ No newline at end of file +}); + +describe('GitOps Account - Update', () => { + beforeAll(setupDatabase); + + test('GITOPS_ACCOUNT_SUITE - Should update account', async () => { + // given + const expectedResponse = JSON.parse(JSON.stringify(VALID_UPDATE_REQUEST)); + expectedResponse.token = '...123'; + + const postStub = sinon.stub(axios, 'put').resolves({ + status: 200, + data: expectedResponse + }); + + // test + const req = await request(app) + .put('/gitops/v1/account') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_UPDATE_REQUEST) + .expect(200); + + // assert + expect(req.body).toMatchObject(expectedResponse); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - error updating account', async () => { + // given + const postStub = sinon.stub(axios, 'put').resolves({ + status: 500, + data: { + error: 'Error updating account' + } + }); + + // test + const req = await request(app) + .put('/gitops/v1/account') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_UPDATE_REQUEST) + .expect(500); + + // assert + expect(req.body.error).toBe('Account update failed'); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - unauthorized', async () => { + // given + const postStub = sinon.stub(axios, 'put').resolves({ + status: 401, + data: { + error: 'Invalid token' + } + }); + + // test + const req = await request(app) + .put('/gitops/v1/account') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_UPDATE_REQUEST) + .expect(500); + + // assert + expect(req.body.error).toBe('Account update failed'); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - missing domain.id', async () => { + const payload = JSON.parse(JSON.stringify(VALID_UPDATE_REQUEST)); + delete payload.domain.id; + + const req = await request(app) + .put('/gitops/v1/account') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(payload) + .expect(422); + + expect(req.body.errors[0].msg).toBe('Invalid domain ID'); + }); + +}) + +describe('GitOps Account - Update Token', () => { + beforeAll(setupDatabase); + + test('GITOPS_ACCOUNT_SUITE - Should update account token', async () => { + // given + const expectedResponse = JSON.parse(JSON.stringify(VALID_UPDATE_REQUEST)); + expectedResponse.token = '...123'; + + const postStub = sinon.stub(axios, 'put').resolves({ + status: 200, + data: expectedResponse + }); + + // test + const req = await request(app) + .put('/gitops/v1/account/token') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_TOKEN_UPDATE_REQUEST) + .expect(200); + + // assert + expect(req.body).toMatchObject(expectedResponse); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - error updating account token', async () => { + // given + const postStub = sinon.stub(axios, 'put').resolves({ + status: 500, + data: { + error: 'Error updating account token' + } + }); + + // test + const req = await request(app) + .put('/gitops/v1/account/token') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_TOKEN_UPDATE_REQUEST) + .expect(500); + + // assert + expect(req.body.error).toBe('Account token update failed'); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - unauthorized', async () => { + // given + const postStub = sinon.stub(axios, 'put').resolves({ + status: 401, + data: { + error: 'Invalid token' + } + }); + + // test + const req = await request(app) + .put('/gitops/v1/account/token') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_TOKEN_UPDATE_REQUEST) + .expect(500); + + // assert + expect(req.body.error).toBe('Account token update failed'); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - missing domain.id', async () => { + const payload = JSON.parse(JSON.stringify(VALID_TOKEN_UPDATE_REQUEST)); + delete payload.domain.id; + + const req = await request(app) + .put('/gitops/v1/account/token') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(payload) + .expect(422); + + expect(req.body.errors[0].msg).toBe('Invalid domain ID'); + }); +}); + +describe('GitOps Account - Force sync', () => { + beforeAll(setupDatabase); + + test('GITOPS_ACCOUNT_SUITE - Should force sync account', async () => { + // given + const expectedResponse = JSON.parse(JSON.stringify(VALID_FORCE_SYNC_REQUEST)); + expectedResponse.token = '...123'; + + const postStub = sinon.stub(axios, 'put').resolves({ + status: 200, + data: expectedResponse + }); + + // test + const req = await request(app) + .put('/gitops/v1/account/forcesync') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_FORCE_SYNC_REQUEST) + .expect(200); + + // assert + expect(req.body).toMatchObject(expectedResponse); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - error forcing sync account', async () => { + // given + const postStub = sinon.stub(axios, 'put').resolves({ + status: 500, + data: { + error: 'Error forcing sync account' + } + }); + + // test + const req = await request(app) + .put('/gitops/v1/account/forcesync') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_FORCE_SYNC_REQUEST) + .expect(500); + + // assert + expect(req.body.error).toBe('Account force sync failed'); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - unauthorized', async () => { + // given + const postStub = sinon.stub(axios, 'put').resolves({ + status: 401, + data: { + error: 'Invalid token' + } + }); + + // test + const req = await request(app) + .put('/gitops/v1/account/forcesync') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(VALID_FORCE_SYNC_REQUEST) + .expect(500); + + // assert + expect(req.body.error).toBe('Account force sync failed'); + postStub.restore(); + }); + + test('GITOPS_ACCOUNT_SUITE - Should return error - missing domain.id', async () => { + const payload = JSON.parse(JSON.stringify(VALID_FORCE_SYNC_REQUEST)); + delete payload.domain.id; + + const req = await request(app) + .put('/gitops/v1/account/forcesync') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(payload) + .expect(422); + + expect(req.body.errors[0].msg).toBe('Invalid domain ID'); + }); +});