diff --git a/server/app.js b/server/app.js index a3b347d9a..90b3217b5 100644 --- a/server/app.js +++ b/server/app.js @@ -10,7 +10,9 @@ const transactionRoutes = require('./routes/transactions'); const automationSchedulerRoutes = require('./routes/automation'); const path = require('path'); const apolloServer = require('./graphql-server'); -const setupMockAutomationSchedulerServer = require('./tests/util/mock-automation-scheduler-server'); +const { + setupMockAutomationSchedulerServer +} = require('./tests/util/mock-automation-scheduler-server'); const transactionMiddleware = require('./middleware/transactionMiddleware'); const app = express(); diff --git a/server/controllers/AutomationController.js b/server/controllers/AutomationController.js index 9d0d3c0c7..62e49a039 100644 --- a/server/controllers/AutomationController.js +++ b/server/controllers/AutomationController.js @@ -30,9 +30,6 @@ const getGraphQLContext = require('../graphql-context'); const httpAgent = new http.Agent({ family: 4 }); const axiosConfig = { - headers: { - 'x-automation-secret': process.env.AUTOMATION_SCHEDULER_SECRET - }, timeout: 1000, httpAgent }; @@ -267,7 +264,8 @@ const updateJobResults = async (req, res) => { } = {} } = req.body; - const job = await getCollectionJobById({ id, transaction }); + const job = + req.collectionJob ?? (await getCollectionJobById({ id, transaction })); if (!job) { throwNoJobFoundError(id); } diff --git a/server/graphql-context.js b/server/graphql-context.js index 9882cd260..2f10719ca 100644 --- a/server/graphql-context.js +++ b/server/graphql-context.js @@ -9,7 +9,6 @@ const getGraphQLContext = ({ req }) => { const atLoader = AtLoader(); const browserLoader = BrowserLoader(); - return { user, atLoader, browserLoader, transaction }; }; diff --git a/server/middleware/verifyAutomationScheduler.js b/server/middleware/verifyAutomationScheduler.js index 8481e6c37..72510c54a 100644 --- a/server/middleware/verifyAutomationScheduler.js +++ b/server/middleware/verifyAutomationScheduler.js @@ -1,10 +1,25 @@ -const verifyAutomationScheduler = (req, res, next) => { +const { + getCollectionJobById +} = require('../models/services/CollectionJobService'); + +const verifyAutomationScheduler = async (req, res, next) => { const incomingSecret = req.headers['x-automation-secret']; + const { jobID: id } = req.params; + + if (!id) return res.status(404).json({ error: 'unknown jobId param' }); + const job = await getCollectionJobById({ + id, + transaction: req.transaction + }); + if (!job) + return res + .status(404) + .json({ error: `Could not find job with jobId: ${id}` }); + + // store the collection job on the request to avoid a second lookup + req.collectionJob = job; - if ( - incomingSecret && - incomingSecret === process.env.AUTOMATION_SCHEDULER_SECRET - ) { + if (incomingSecret && incomingSecret === job.secret) { next(); } else { res.status(403).json({ error: 'Unauthorized' }); diff --git a/server/migrations/20240525041559-addCollectionJobSecret.js b/server/migrations/20240525041559-addCollectionJobSecret.js new file mode 100644 index 000000000..a74e9f122 --- /dev/null +++ b/server/migrations/20240525041559-addCollectionJobSecret.js @@ -0,0 +1,27 @@ +'use strict'; +const uuid = require('uuid'); + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.addColumn( + 'CollectionJob', + 'secret', + { + type: Sequelize.DataTypes.UUID, + allowNull: false, + defaultValue: uuid.NIL + }, + { transaction } + ); + }); + }, + async down(queryInterface) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.removeColumn('CollectionJob', 'secret', { + transaction + }); + }); + } +}; diff --git a/server/models/CollectionJob.js b/server/models/CollectionJob.js index 4fc381126..8122099df 100644 --- a/server/models/CollectionJob.js +++ b/server/models/CollectionJob.js @@ -1,5 +1,5 @@ const { COLLECTION_JOB_STATUS } = require('../util/enums'); - +const { v4: uuid } = require('uuid'); const MODEL_NAME = 'CollectionJob'; module.exports = function (sequelize, DataTypes) { @@ -31,9 +31,20 @@ module.exports = function (sequelize, DataTypes) { onDelete: 'SET NULL', allowNull: true, unique: true + }, + secret: { + type: DataTypes.UUID, + allowNull: false } }, { + hooks: { + beforeValidate: job => { + if (!job.secret) { + job.secret = uuid(); + } + } + }, timestamps: false, tableName: MODEL_NAME } diff --git a/server/models/services/CollectionJobService.js b/server/models/services/CollectionJobService.js index 1739a1f83..251faee18 100644 --- a/server/models/services/CollectionJobService.js +++ b/server/models/services/CollectionJobService.js @@ -19,22 +19,20 @@ const { } = require('./TestPlanRunService'); const { getTestPlanReportById } = require('./TestPlanReportService'); const { HttpQueryError } = require('apollo-server-core'); -const { default: axios } = require('axios'); + const { default: createGithubWorkflow, isEnabled: isGithubWorkflowEnabled } = require('../../services/GithubWorkflowService'); + +const { + startCollectionJobSimulation +} = require('../../tests/util/mock-automation-scheduler-server'); + const runnableTestsResolver = require('../../resolvers/TestPlanReport/runnableTestsResolver'); const getGraphQLContext = require('../../graphql-context'); const { getBotUserByAtId } = require('./UserService'); -const axiosConfig = { - headers: { - 'x-automation-secret': process.env.AUTOMATION_SCHEDULER_SECRET - }, - timeout: 1000 -}; - // association helpers to be included with Models' results /** @@ -252,7 +250,6 @@ const createCollectionJob = async ({ })), transaction }); - return ModelService.getById(CollectionJob, { id: collectionJobResult.id, attributes: collectionJobAttributes, @@ -374,6 +371,7 @@ const getCollectionJobs = async ({ ], pagination, transaction + // logging: console.log }); }; @@ -393,17 +391,7 @@ const triggerWorkflow = async (job, testIds, { transaction }) => { // TODO: pass the reduced list of testIds along / deal with them somehow await createGithubWorkflow({ job, directory, gitSha }); } else { - await axios.post( - `${process.env.AUTOMATION_SCHEDULER_URL}/jobs/new`, - { - testPlanVersionGitSha: gitSha, - testIds, - testPlanName: directory, - jobId: job.id, - transactionId: transaction.id - }, - axiosConfig - ); + await startCollectionJobSimulation(job, transaction); } } catch (error) { console.error(error); diff --git a/server/package.json b/server/package.json index 2f19873e4..492afae16 100644 --- a/server/package.json +++ b/server/package.json @@ -56,6 +56,7 @@ "sequelize": "^6.28.0", "shared": "1.0.0", "supertest-session": "^4.1.0", + "uuid": "^10.0.0", "vhost": "^3.0.2" }, "devDependencies": { diff --git a/server/services/GithubWorkflowService.js b/server/services/GithubWorkflowService.js index e4e103a51..3b196556b 100644 --- a/server/services/GithubWorkflowService.js +++ b/server/services/GithubWorkflowService.js @@ -140,7 +140,7 @@ const createGithubWorkflow = async ({ job, directory, gitSha }) => { const inputs = { callback_url: `https://${callbackUrlHostname}/api/jobs/${job.id}/test/:testRowNumber`, status_url: `https://${callbackUrlHostname}/api/jobs/${job.id}`, - callback_header: `x-automation-secret:${process.env.AUTOMATION_SCHEDULER_SECRET}`, + callback_header: `x-automation-secret:${job.secret}`, work_dir: `tests/${directory}`, aria_at_ref: gitSha }; diff --git a/server/tests/integration/automation-scheduler.test.js b/server/tests/integration/automation-scheduler.test.js index 0e25b318c..9416eb324 100644 --- a/server/tests/integration/automation-scheduler.test.js +++ b/server/tests/integration/automation-scheduler.test.js @@ -1,10 +1,11 @@ const startSupertestServer = require('../util/api-server'); const automationRoutes = require('../../routes/automation'); -const setupMockAutomationSchedulerServer = require('../util/mock-automation-scheduler-server'); +const { + setupMockAutomationSchedulerServer +} = require('../util/mock-automation-scheduler-server'); const db = require('../../models/index'); const { query, mutate } = require('../util/graphql-test-utilities'); const dbCleaner = require('../util/db-cleaner'); -const { default: axios } = require('axios'); const { getCollectionJobById } = require('../../models/services/CollectionJobService'); @@ -14,22 +15,20 @@ const BrowserLoader = require('../../models/loaders/BrowserLoader'); const getGraphQLContext = require('../../graphql-context'); const { COLLECTION_JOB_STATUS } = require('../../util/enums'); -let mockAutomationSchedulerServer; let apiServer; let sessionAgent; -const testPlanReportId = '4'; +const testPlanReportId = '18'; beforeAll(async () => { apiServer = await startSupertestServer({ pathToRoutes: [['/api/jobs', automationRoutes]] }); sessionAgent = apiServer.sessionAgent; - mockAutomationSchedulerServer = await setupMockAutomationSchedulerServer(); + await setupMockAutomationSchedulerServer(); }); afterAll(async () => { - await mockAutomationSchedulerServer.tearDown(); await apiServer.tearDown(); await db.sequelize.close(); }); @@ -46,6 +45,7 @@ const getTestPlanReport = async (id, { transaction }) => finalizedTestResults { test { id + rowNumber } atVersion { name @@ -222,6 +222,11 @@ const deleteCollectionJobByMutation = async (jobId, { transaction }) => { transaction } ); +const getJobSecret = async (jobId, { transaction }) => { + const job = await getCollectionJobById({ id: jobId, transaction }); + return job.secret; +}; + describe('Automation controller', () => { it('should schedule a new job', async () => { await dbCleaner(async transaction => { @@ -235,68 +240,6 @@ describe('Automation controller', () => { }); }); - it('should schedule a new job and correctly construct test data for automation scheduler', async () => { - await dbCleaner(async transaction => { - const axiosPostMock = jest.spyOn(axios, 'post').mockResolvedValue({ - data: { id: '999', status: 'QUEUED' } - }); - - const { - testPlanReport: { - runnableTests, - testPlanVersion: { - gitSha: testPlanVersionGitSha, - testPlan: { directory: testPlanName } - } - } - } = await query( - ` - query { - testPlanReport(id: "${testPlanReportId}") { - runnableTests { - id - } - testPlanVersion { - testPlan { - directory - } - gitSha - } - } - } - `, - { transaction } - ); - - const testIds = runnableTests.map(({ id }) => id); - - const collectionJob = await scheduleCollectionJobByMutation({ - transaction - }); - - const expectedRequestBody = { - testPlanVersionGitSha, - testIds, - testPlanName, - jobId: parseInt(collectionJob.scheduleCollectionJob.id), - transactionId: transaction.id - }; - - expect(axiosPostMock).toHaveBeenCalledWith( - `${process.env.AUTOMATION_SCHEDULER_URL}/jobs/new`, - expectedRequestBody, - { - headers: { - 'x-automation-secret': process.env.AUTOMATION_SCHEDULER_SECRET - }, - timeout: 1000 - } - ); - - axiosPostMock.mockRestore(); - }); - }); - it('should cancel a job and all remaining tests', async () => { await dbCleaner(async transaction => { const { scheduleCollectionJob: job } = @@ -355,25 +298,29 @@ describe('Automation controller', () => { }); it('should not update a job status without verification', async () => { - await dbCleaner(async transaction => { + await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); - const response = await sessionAgent.post(`/api/jobs/${job.id}`); - expect(response.statusCode).toBe(403); + const response = await sessionAgent + .post(`/api/jobs/${job.id}`) + .set('x-transaction-id', transaction.id); expect(response.body).toEqual({ error: 'Unauthorized' }); + expect(response.statusCode).toBe(403); }); }); it('should fail to update a job status with invalid status', async () => { - await dbCleaner(async transaction => { + await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const response = await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'INVALID' }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET); + .set('x-automation-secret', secret) + .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(400); expect(response.body).toEqual({ error: 'Invalid status: INVALID' @@ -396,10 +343,11 @@ describe('Automation controller', () => { await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const response = await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING' }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); const { body } = response; expect(response.statusCode).toBe(200); @@ -427,13 +375,14 @@ describe('Automation controller', () => { await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const response = await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'CANCELLED', externalLogsUrl: 'https://www.aol.com/' }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); const { body } = response; expect(response.statusCode).toBe(200); @@ -463,6 +412,7 @@ describe('Automation controller', () => { await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const collectionJob = await getCollectionJobById({ id: job.id, transaction @@ -470,7 +420,7 @@ describe('Automation controller', () => { await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING' }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); const automatedTestResponse = 'AUTOMATED TEST RESPONSE'; const ats = await AtLoader().getAll({ transaction }); @@ -484,11 +434,11 @@ describe('Automation controller', () => { ); const { tests } = collectionJob.testPlanRun.testPlanReport.testPlanVersion; - const testResultsNumber = collectionJob.testPlanRun.testResults.length; - const selectedTestIndex = 0; - const selectedTestRowNumber = 1; + const selectedTestIndex = 2; const selectedTest = tests[selectedTestIndex]; + const selectedTestRowNumber = selectedTest.rowNumber; + const numberOfScenarios = selectedTest.scenarios.filter( scenario => scenario.atId === at.id ).length; @@ -503,7 +453,7 @@ describe('Automation controller', () => { }, responses: new Array(numberOfScenarios).fill(automatedTestResponse) }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); const storedTestPlanRun = await getTestPlanRun( @@ -511,7 +461,6 @@ describe('Automation controller', () => { { transaction } ); const { testResults } = storedTestPlanRun.testPlanRun; - expect(testResults.length).toEqual(testResultsNumber + 1); testResults.forEach(testResult => { expect(testResult.test.id).toEqual(selectedTest.id); expect(testResult.atVersion.name).toEqual(at.atVersions[0].name); @@ -551,6 +500,7 @@ describe('Automation controller', () => { transaction, reportId: '19' }); + const secret = await getJobSecret(job.id, { transaction }); const collectionJob = await getCollectionJobById({ id: job.id, transaction @@ -558,7 +508,7 @@ describe('Automation controller', () => { await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING' }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); const automatedTestResponse = 'AUTOMATED TEST RESPONSE'; const ats = await AtLoader().getAll({ transaction }); @@ -593,7 +543,7 @@ describe('Automation controller', () => { }, responses: new Array(numberOfScenarios).fill(automatedTestResponse) }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); const storedTestPlanRun = await getTestPlanRun( @@ -638,6 +588,7 @@ describe('Automation controller', () => { await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const collectionJob = await getCollectionJobById({ id: job.id, transaction @@ -647,20 +598,20 @@ describe('Automation controller', () => { await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING', externalLogsUrl }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); const { tests } = collectionJob.testPlanRun.testPlanReport.testPlanVersion; - const selectedTestIndex = 0; - const selectedTestRowNumber = 1; + const selectedTestIndex = 2; const selectedTest = tests[selectedTestIndex]; + const selectedTestRowNumber = selectedTest.rowNumber; let response = await sessionAgent .post(`/api/jobs/${job.id}/test/${selectedTestRowNumber}`) .send({ status: COLLECTION_JOB_STATUS.RUNNING }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -689,7 +640,7 @@ describe('Automation controller', () => { .send({ status: COLLECTION_JOB_STATUS.ERROR }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -700,7 +651,7 @@ describe('Automation controller', () => { // missing it is not overwritten/emptied. status: COLLECTION_JOB_STATUS.ERROR }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -727,6 +678,7 @@ describe('Automation controller', () => { await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const collectionJob = await getCollectionJobById({ id: job.id, transaction @@ -736,20 +688,20 @@ describe('Automation controller', () => { await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING', externalLogsUrl }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); const { tests } = collectionJob.testPlanRun.testPlanReport.testPlanVersion; - const selectedTestIndex = 0; - const selectedTestRowNumber = 1; + const selectedTestIndex = 2; const selectedTest = tests[selectedTestIndex]; + const selectedTestRowNumber = selectedTest.rowNumber; let response = await sessionAgent .post(`/api/jobs/${job.id}/test/${selectedTestRowNumber}`) .send({ status: COLLECTION_JOB_STATUS.RUNNING }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -780,7 +732,7 @@ describe('Automation controller', () => { // missing it is not overwritten/emptied. status: COLLECTION_JOB_STATUS.ERROR }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -807,6 +759,7 @@ describe('Automation controller', () => { await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const collectionJob = await getCollectionJobById({ id: job.id, transaction @@ -816,20 +769,20 @@ describe('Automation controller', () => { await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING', externalLogsUrl }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); const { tests } = collectionJob.testPlanRun.testPlanReport.testPlanVersion; - const selectedTestIndex = 0; - const selectedTestRowNumber = 1; + const selectedTestIndex = 2; const selectedTest = tests[selectedTestIndex]; + const selectedTestRowNumber = selectedTest.rowNumber; let response = await sessionAgent .post(`/api/jobs/${job.id}/test/${selectedTestRowNumber}`) .send({ status: COLLECTION_JOB_STATUS.RUNNING }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -860,7 +813,7 @@ describe('Automation controller', () => { // missing it is not overwritten/emptied. status: COLLECTION_JOB_STATUS.CANCELLED }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -880,29 +833,32 @@ describe('Automation controller', () => { await apiServer.sessionAgentDbCleaner(async transaction => { const { scheduleCollectionJob: job } = await scheduleCollectionJobByMutation({ transaction }); + const secret = await getJobSecret(job.id, { transaction }); const collectionJob = await getCollectionJobById({ id: job.id, transaction }); // flag overall job as RUNNING const externalLogsUrl = 'https://example.com/test/log/url'; - await sessionAgent + let response = await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING', externalLogsUrl }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); + expect(response.statusCode).toBe(200); + const { tests } = collectionJob.testPlanRun.testPlanReport.testPlanVersion; - const selectedTestIndex = 0; - const selectedTestRowNumber = 1; + const selectedTestIndex = 2; const selectedTest = tests[selectedTestIndex]; - let response = await sessionAgent + const selectedTestRowNumber = selectedTest.rowNumber; + response = await sessionAgent .post(`/api/jobs/${job.id}/test/${selectedTestRowNumber}`) .send({ status: COLLECTION_JOB_STATUS.RUNNING }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -933,7 +889,7 @@ describe('Automation controller', () => { // missing it is not overwritten/emptied. status: COLLECTION_JOB_STATUS.COMPLETED }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); @@ -979,7 +935,6 @@ describe('Automation controller', () => { { transaction } ); const selectedTestIndex = 0; - const selectedTestRowNumber = 1; const { at, browser } = testPlanReport; const historicalTestResult = @@ -996,13 +951,23 @@ describe('Automation controller', () => { id: job.id, transaction }); - await sessionAgent + const { secret } = collectionJob; + + const { tests } = + collectionJob.testPlanRun.testPlanReport.testPlanVersion; + + const selectedTestRowNumber = tests.find( + t => t.id === historicalTestResult.test.id + ).rowNumber; + + let response = await sessionAgent .post(`/api/jobs/${job.id}`) .send({ status: 'RUNNING' }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); + expect(response.statusCode).toBe(200); - const response = await sessionAgent + response = await sessionAgent .post(`/api/jobs/${job.id}/test/${selectedTestRowNumber}`) .send({ capabilities: { @@ -1013,7 +978,7 @@ describe('Automation controller', () => { }, responses: historicalResponses }) - .set('x-automation-secret', process.env.AUTOMATION_SCHEDULER_SECRET) + .set('x-automation-secret', secret) .set('x-transaction-id', transaction.id); expect(response.statusCode).toBe(200); diff --git a/server/tests/util/mock-automation-scheduler-server.js b/server/tests/util/mock-automation-scheduler-server.js index 839fd3d16..adab84a4d 100644 --- a/server/tests/util/mock-automation-scheduler-server.js +++ b/server/tests/util/mock-automation-scheduler-server.js @@ -1,240 +1,234 @@ -const { GracefulShutdownManager } = require('@moebius/http-graceful-shutdown'); -const express = require('express'); -const { - verifyAutomationScheduler -} = require('../../middleware/verifyAutomationScheduler'); const { COLLECTION_JOB_STATUS } = require('../../util/enums'); const { default: axios } = require('axios'); const { gql } = require('apollo-server-core'); -const { axiosConfig } = require('../../controllers/AutomationController'); -const { - getTransactionById -} = require('../../middleware/transactionMiddleware'); -const { query } = require('../util/graphql-test-utilities'); // 0 = no chance of test errors, 1 = always errors const TEST_ERROR_CHANCE = 0; -const setupMockAutomationSchedulerServer = async () => { - const app = express(); - app.use(express.json()); - app.use(verifyAutomationScheduler); - - let shutdownManager; - await new Promise(resolve => { - const listener = app.listen(process.env.AUTOMATION_SCHEDULER_PORT, resolve); - shutdownManager = new GracefulShutdownManager(listener); - }); +let apolloServer, axiosConfig; - const timeout = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); +const timeout = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); +let mockSchedulerEnabled = false; +const setupMockAutomationSchedulerServer = async () => { + mockSchedulerEnabled = true; + axiosConfig = require('../../controllers/AutomationController').axiosConfig; + apolloServer = require('../../graphql-server'); +}; - const simulateJobStatusUpdate = async (jobId, newStatus) => { +const simulateJobStatusUpdate = async (jobId, newStatus, headers) => { + if (!mockSchedulerEnabled) throw new Error('mock scheduler is not enabled'); + try { await axios.post( `${process.env.APP_SERVER}/api/jobs/${jobId}`, { status: newStatus }, - axiosConfig + { + ...axiosConfig, + headers + } ); - }; + } catch (err) { + console.error('error delivering mock job status update', err); + } +}; - const simulateTestStatusUpdate = async (jobId, testId, newStatus) => { +const simulateTestStatusUpdate = async (jobId, testId, newStatus, headers) => { + if (!mockSchedulerEnabled) throw new Error('mock scheduler is not enabled'); + try { await axios.post( `${process.env.APP_SERVER}/api/jobs/${jobId}/test/${testId}`, { status: newStatus }, - axiosConfig + { + ...axiosConfig, + headers + } ); - }; + } catch (err) { + console.error('error delivering mock data', err); + } +}; - const simulateResultCompletion = async ( - tests, - atName, - atVersionName, - browserName, - browserVersionName, - jobId, - currentTestIndex, - isV2 - ) => { - const currentTest = tests[currentTestIndex]; - const { scenarios, assertions } = currentTest; - - const responses = []; - scenarios.forEach(() => { - assertions.forEach(() => { - responses.push('Local development simulated output'); - }); +const simulateResultCompletion = async ( + tests, + atName, + atVersionName, + browserName, + browserVersionName, + jobId, + currentTestIndex, + isV2, + headers +) => { + if (!mockSchedulerEnabled) throw new Error('mock scheduler is not enabled'); + const currentTest = tests[currentTestIndex]; + const { scenarios, assertions } = currentTest; + + const responses = []; + scenarios.forEach(() => { + assertions.forEach(() => { + responses.push('Local development simulated output'); }); + }); - const testResult = { - capabilities: { - atName, - atVersion: atVersionName, - browserName, - browserVersion: browserVersionName - }, - responses - }; + const testResult = { + capabilities: { + atName, + atVersion: atVersionName, + browserName, + browserVersion: browserVersionName + }, + responses + }; - try { + try { + await simulateTestStatusUpdate( + jobId, + currentTest.rowNumber, + COLLECTION_JOB_STATUS.RUNNING, + headers + ); + await timeout(Math.random() * 2000); + + if (Math.random() < TEST_ERROR_CHANCE) { await simulateTestStatusUpdate( jobId, currentTest.rowNumber, - COLLECTION_JOB_STATUS.RUNNING + COLLECTION_JOB_STATUS.ERROR, + headers ); - await timeout(Math.random() * 2000); - - if (Math.random() < TEST_ERROR_CHANCE) { - await simulateTestStatusUpdate( - jobId, - currentTest.rowNumber, - COLLECTION_JOB_STATUS.ERROR - ); - return simulateJobStatusUpdate(jobId, COLLECTION_JOB_STATUS.ERROR); - } else { - await axios.post( - `${process.env.APP_SERVER}/api/jobs/${jobId}/test/${currentTest.rowNumber}`, - testResult, - axiosConfig - ); - } - - if (currentTestIndex < tests.length - 1) { - await timeout(Math.random() * 5000); - return simulateResultCompletion( - tests, - atName, - atVersionName, - browserName, - browserVersionName, - jobId, - currentTestIndex + 1, - isV2 - ); - } else { - simulateJobStatusUpdate(jobId, COLLECTION_JOB_STATUS.COMPLETED); - } - } catch (error) { - console.error('Error simulating collection job', error); - } - }; - - app.post('/jobs/new', async (req, res) => { - if (process.env.ENVIRONMENT === 'test') { - return res.json({ - status: COLLECTION_JOB_STATUS.QUEUED - }); - } else { - // Local development must simulate posting results - const { jobId, transactionId } = req.body; - const transaction = getTransactionById(transactionId); - const data = await query( - gql` - query { - collectionJob(id: "${jobId}") { - testPlanRun { - testPlanReport { - testPlanVersion { - metadata - gitSha - } - at { - name - atVersions { - name - } - } - browser { - name - browserVersions { - name - } - } - runnableTests { - id - rowNumber - scenarios { - id - } - assertions { - id - } - } - } - } - } - } - `, - { transaction } + return simulateJobStatusUpdate( + jobId, + COLLECTION_JOB_STATUS.ERROR, + headers ); - const { collectionJob } = data; - const { testPlanReport } = collectionJob.testPlanRun; - const { testPlanVersion } = testPlanReport; - - const browserName = testPlanReport.browser.name; - const browserVersionName = testPlanReport.browser.browserVersions[0].name; - - const atName = testPlanReport.at.name; - const atVersionName = testPlanReport.at.atVersions[0].name; - const { runnableTests } = testPlanReport; - - const isV2 = testPlanVersion.metadata?.testFormatVersion === 2; - - setTimeout( - () => simulateJobStatusUpdate(jobId, COLLECTION_JOB_STATUS.RUNNING), - 1000 + } else { + await axios.post( + `${process.env.APP_SERVER}/api/jobs/${jobId}/test/${currentTest.rowNumber}`, + testResult, + { + ...axiosConfig, + headers + } ); + } - setTimeout( - () => - simulateResultCompletion( - runnableTests, - atName, - atVersionName, - browserName, - browserVersionName, - jobId, - 0, - isV2 - ), - 3000 + if (currentTestIndex < tests.length - 1) { + await timeout(Math.random() * 5000); + return simulateResultCompletion( + tests, + atName, + atVersionName, + browserName, + browserVersionName, + jobId, + currentTestIndex + 1, + isV2, + headers ); - return res.json({ - id: jobId, - status: COLLECTION_JOB_STATUS.QUEUED - }); + } else { + simulateJobStatusUpdate(jobId, COLLECTION_JOB_STATUS.COMPLETED, headers); } - }); + } catch (error) { + console.error('Error simulating collection job', error); + } +}; - app.post('/jobs/:jobID/cancel', (req, res) => { - return res.json({ - id: req.params.jobID, - status: COLLECTION_JOB_STATUS.CANCELLED - }); - }); +const startCollectionJobSimulation = async (job, transaction) => { + if (!mockSchedulerEnabled) throw new Error('mock scheduler is not enabled'); + if (process.env.ENVIRONMENT === 'test') { + // stub behavior in test suite + return { status: COLLECTION_JOB_STATUS.QUEUED }; + } else { + const { data } = await apolloServer.executeOperation( + { + query: gql` + query { + collectionJob(id: "${job.id}") { + id + testPlanRun { + testPlanReport { + testPlanVersion { + metadata + gitSha + } + at { + name + atVersions { + name + } + } + browser { + name + browserVersions { + name + } + } + runnableTests { + id + rowNumber + scenarios { + id + } + assertions { + id + } + } + } + } + } + }` + }, + { req: { transaction } } + ); + const headers = { + 'x-automation-secret': job.secret + }; + const { collectionJob } = data; + const jobId = collectionJob.id; + const { testPlanReport } = collectionJob.testPlanRun; + const { testPlanVersion } = testPlanReport; - app.post('/jobs/:jobID/restart', (req, res) => { - return res.json({ - id: req.params.jobID, - status: COLLECTION_JOB_STATUS.QUEUED - }); - }); + const browserName = testPlanReport.browser.name; + const browserVersionName = testPlanReport.browser.browserVersions[0].name; - app.get('/jobs/:jobID/log', (req, res) => { - return res.json({ id: req.params.jobID, log: 'TEST LOG' }); - }); + const atName = testPlanReport.at.name; + const atVersionName = testPlanReport.at.atVersions[0].name; + const { runnableTests } = testPlanReport; - const tearDown = async () => { - await new Promise(resolve => { - shutdownManager.terminate(resolve); - }); - }; + const isV2 = testPlanVersion.metadata?.testFormatVersion === 2; - return { - tearDown - }; + setTimeout( + () => + simulateJobStatusUpdate(jobId, COLLECTION_JOB_STATUS.RUNNING, headers), + 1000 + ); + + setTimeout( + () => + simulateResultCompletion( + runnableTests, + atName, + atVersionName, + browserName, + browserVersionName, + jobId, + 0, + isV2, + headers + ), + 3000 + ); + return { + id: jobId, + status: COLLECTION_JOB_STATUS.QUEUED + }; + } }; -module.exports = setupMockAutomationSchedulerServer; +module.exports = { + setupMockAutomationSchedulerServer, + startCollectionJobSimulation +}; diff --git a/yarn.lock b/yarn.lock index 8dc1fdef2..7a9ddb82b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17285,6 +17285,11 @@ uuid-browser@^3.1.0: resolved "https://registry.yarnpkg.com/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410" integrity sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg== +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"