Skip to content

Commit 9c54c68

Browse files
committed
add tests
1 parent d309e3d commit 9c54c68

File tree

16 files changed

+246
-171
lines changed

16 files changed

+246
-171
lines changed

lambdas/functions/webhook/src/ConfigLoader.test.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getParameter } from '@aws-github-runner/aws-ssm-util';
22
import { ConfigWebhook, ConfigWebhookEventBridge, ConfigDispatcher } from './ConfigLoader';
33
import { mocked } from 'jest-mock';
44
import { logger } from '@aws-github-runner/aws-powertools-util';
5+
import { MatcherConfig, RunnerMatcherConfig } from './sqs';
56

67
jest.mock('@aws-github-runner/aws-ssm-util');
78

@@ -93,7 +94,7 @@ describe('ConfigLoader Tests', () => {
9394
describe('ConfigWebhook', () => {
9495
it('should load config successfully', async () => {
9596
process.env.REPOSITORY_ALLOW_LIST = '["repo1", "repo2"]';
96-
process.env.WORKFLOW_JOB_EVENT_SECONDARY_QUEUE = 'secondary-queue';
97+
process.env.SQS_WORKFLOW_JOB_QUEUE = 'secondary-queue';
9798
process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config';
9899
process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET = '/path/to/webhook/secret';
99100
const matcherConfig = [
@@ -209,18 +210,17 @@ describe('ConfigLoader Tests', () => {
209210
process.env.REPOSITORY_ALLOW_LIST = '["repo1", "repo2"]';
210211
process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config';
211212

212-
const matcherConfig = [
213+
const matcherConfig: RunnerMatcherConfig[] = [
213214
{
214-
id: '1',
215-
arn: 'arn:aws:sqs:us-east-1:123456789012:queue1',
216-
fifo: false,
215+
arn: 'arn:aws:sqs:eu-central-1:123456:npalm-default-queued-builds',
216+
fifo: true,
217+
id: 'https://sqs.eu-central-1.amazonaws.com/123456/npalm-default-queued-builds',
217218
matcherConfig: {
218-
labelMatchers: [['label1', 'label2']],
219219
exactMatch: true,
220+
labelMatchers: [['default', 'example', 'linux', 'self-hosted', 'x64']],
220221
},
221222
},
222223
];
223-
224224
mocked(getParameter).mockImplementation(async (paramPath: string) => {
225225
if (paramPath === '/path/to/matcher/config') {
226226
return JSON.stringify(matcherConfig);
@@ -243,5 +243,20 @@ describe('ConfigLoader Tests', () => {
243243
'Failed to load config: Failed to load parameter for matcherConfig from path undefined: Parameter undefined not found',
244244
);
245245
});
246+
247+
it('should throw an error if runner matcher config is empty.', async () => {
248+
process.env.REPOSITORY_ALLOW_LIST = '["repo1", "repo2"]';
249+
process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config';
250+
251+
mocked(getParameter).mockImplementation(async (paramPath: string) => {
252+
if (paramPath === '/path/to/matcher/config') {
253+
return JSON.stringify('');
254+
}
255+
return '';
256+
});
257+
258+
await expect(ConfigDispatcher.load()).rejects.toThrow('ailed to load config: Matcher config is empty');
259+
260+
});
246261
});
247262
});

lambdas/functions/webhook/src/ConfigLoader.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class ConfigWebhook extends BaseConfig {
8686

8787
async loadConfig(): Promise<void> {
8888
this.loadEnvVar(process.env.REPOSITORY_ALLOW_LIST, 'repositoryAllowList', []);
89-
this.loadEnvVar(process.env.WORKFLOW_JOB_EVENT_SECONDARY_QUEUE, 'workflowJobEventSecondaryQueue', '');
89+
this.loadEnvVar(process.env.SQS_WORKFLOW_JOB_QUEUE, 'workflowJobEventSecondaryQueue', '');
9090

9191
await Promise.all([
9292
this.loadParameter<MatcherConfig[]>(process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH, 'matcherConfig'),
@@ -110,9 +110,15 @@ export class ConfigWebhookEventBridge extends BaseConfig {
110110
export class ConfigDispatcher extends BaseConfig {
111111
repositoryAllowList: string[] = [];
112112
matcherConfig: RunnerMatcherConfig[] = [];
113+
workflowJobEventSecondaryQueue: string = ''; // Deprecated, not loaded
113114

114115
async loadConfig(): Promise<void> {
115116
this.loadEnvVar(process.env.REPOSITORY_ALLOW_LIST, 'repositoryAllowList', []);
116117
await this.loadParameter<MatcherConfig[]>(process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH, 'matcherConfig');
118+
119+
// check matcherConfig object is not empty
120+
if (this.matcherConfig.length === 0) {
121+
this.configLoadingErrors.push('Matcher config is empty');
122+
}
117123
}
118124
}

lambdas/functions/webhook/src/ConfigResolver.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

lambdas/functions/webhook/src/lambda.test.ts

Lines changed: 119 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { logger } from '@aws-github-runner/aws-powertools-util';
22
import { APIGatewayEvent, Context } from 'aws-lambda';
33
import { mocked } from 'jest-mock';
4+
import { WorkflowJobEvent } from '@octokit/webhooks-types';
45

5-
import { githubWebhook } from './lambda';
6-
import { handle } from './webhook';
6+
import { dispatchToRunners, eventBridgeWebhook, githubWebhook } from './lambda';
7+
import { handle, publishOnEventBridge } from './webhook';
78
import ValidationError from './ValidationError';
89
import { getParameter } from '@aws-github-runner/aws-ssm-util';
10+
import { dispatch } from './runners/dispatch';
11+
import { EventWrapper } from './types';
12+
import { rejects } from 'assert';
913

1014
const event: APIGatewayEvent = {
1115
body: JSON.stringify(''),
@@ -73,40 +77,132 @@ const context: Context = {
7377
},
7478
};
7579

80+
81+
jest.mock('./runners/dispatch');
7682
jest.mock('./webhook');
7783
jest.mock('@aws-github-runner/aws-ssm-util');
7884

79-
describe('Test scale up lambda wrapper.', () => {
85+
describe('Test webhook lambda wrapper.', () => {
8086
beforeEach(() => {
87+
// We mock all SSM request to resolve to a non empty array. Since we mock all implemeantions
88+
// relying on the config opbject that is enought to test the handlers.
8189
const mockedGet = mocked(getParameter);
82-
mockedGet.mockResolvedValue('[]');
90+
mockedGet.mockResolvedValue('["abc"]');
91+
jest.clearAllMocks();
8392
});
84-
it('Happy flow, resolve.', async () => {
85-
const mock = mocked(handle);
86-
mock.mockImplementation(() => {
87-
return new Promise((resolve) => {
88-
resolve({ body: 'test', statusCode: 200 });
93+
94+
describe('Test webhook lambda wrapper.', () => {
95+
it('Happy flow, resolve.', async () => {
96+
const mock = mocked(handle);
97+
mock.mockImplementation(() => {
98+
return new Promise((resolve) => {
99+
resolve({ body: 'test', statusCode: 200 });
100+
});
89101
});
102+
103+
const result = await githubWebhook(event, context);
104+
expect(result).toEqual({ body: 'test', statusCode: 200 });
90105
});
91106

92-
const result = await githubWebhook(event, context);
93-
expect(result).toEqual({ body: 'test', statusCode: 200 });
107+
it('An expected error, resolve.', async () => {
108+
const mock = mocked(handle);
109+
mock.mockRejectedValue(new ValidationError(400, 'some error'));
110+
111+
const result = await githubWebhook(event, context);
112+
expect(result).toMatchObject({ body: 'some error', statusCode: 400 });
113+
});
114+
115+
it('Errors are not thrown.', async () => {
116+
const mock = mocked(handle);
117+
const logSpy = jest.spyOn(logger, 'error');
118+
mock.mockRejectedValue(new Error('some error'));
119+
const result = await githubWebhook(event, context);
120+
expect(result).toMatchObject({ body: 'Check the Lambda logs for the error details.', statusCode: 500 });
121+
expect(logSpy).toHaveBeenCalledTimes(1);
122+
});
94123
});
95124

96-
it('An expected error, resolve.', async () => {
97-
const mock = mocked(handle);
98-
mock.mockRejectedValue(new ValidationError(400, 'some error'));
99125

100-
const result = await githubWebhook(event, context);
101-
expect(result).toMatchObject({ body: 'some error', statusCode: 400 });
126+
127+
describe('Lmmbda eventBridgeWebhook.', () => {
128+
beforeEach(() => {
129+
process.env.EVENT_BUS_NAME = 'test';
130+
});
131+
132+
it('Happy flow, resolve.', async () => {
133+
const mock = mocked(publishOnEventBridge);
134+
mock.mockImplementation(() => {
135+
return new Promise((resolve) => {
136+
resolve({ body: 'test', statusCode: 200 });
137+
});
138+
});
139+
140+
const result = await eventBridgeWebhook(event, context);
141+
expect(result).toEqual({ body: 'test', statusCode: 200 });
142+
});
143+
144+
145+
it('Reject events .', async () => {
146+
const mock = mocked(publishOnEventBridge);
147+
mock.mockRejectedValue(new Error('some error'));
148+
149+
mock.mockRejectedValue(new ValidationError(400, 'some error'));
150+
151+
const result = await eventBridgeWebhook(event, context);
152+
expect(result).toMatchObject({ body: 'some error', statusCode: 400 });
153+
});
154+
155+
it('Errors are not thrown.', async () => {
156+
const mock = mocked(publishOnEventBridge);
157+
const logSpy = jest.spyOn(logger, 'error');
158+
mock.mockRejectedValue(new Error('some error'));
159+
const result = await eventBridgeWebhook(event, context);
160+
expect(result).toMatchObject({ body: 'Check the Lambda logs for the error details.', statusCode: 500 });
161+
expect(logSpy).toHaveBeenCalledTimes(1);
162+
});
102163
});
103164

104-
it('Errors are not thrown.', async () => {
105-
const mock = mocked(handle);
106-
const logSpy = jest.spyOn(logger, 'error');
107-
mock.mockRejectedValue(new Error('some error'));
108-
const result = await githubWebhook(event, context);
109-
expect(result).toMatchObject({ body: 'Check the Lambda logs for the error details.', statusCode: 500 });
110-
expect(logSpy).toHaveBeenCalledTimes(1);
165+
describe('Lmmbda dispatchToRunners.', () => {
166+
it('Happy flow, resolve.', async () => {
167+
const mock = mocked(dispatch);
168+
mock.mockImplementation(() => {
169+
return new Promise((resolve) => {
170+
resolve({ body: 'test', statusCode: 200 });
171+
});
172+
});
173+
174+
const testEvent = {
175+
'detail-type': 'workflow_job',
176+
} as unknown as EventWrapper<WorkflowJobEvent>;
177+
178+
await expect(dispatchToRunners(testEvent, context)).resolves.not.toThrow();
179+
});
180+
181+
it('Rejects non workflow_job events.', async () => {
182+
const mock = mocked(dispatch);
183+
mock.mockImplementation(() => {
184+
return new Promise((resolve) => {
185+
resolve({ body: 'test', statusCode: 200 });
186+
});
187+
});
188+
189+
const testEvent = {
190+
'detail-type': 'non_workflow_job',
191+
} as unknown as EventWrapper<WorkflowJobEvent>;
192+
193+
await expect(dispatchToRunners(testEvent, context)).rejects.toThrow('Incorrect Event detail-type only workflow_job is accepted');
194+
});
195+
196+
197+
it('Rejects any event causing an error.', async () => {
198+
const mock = mocked(dispatch);
199+
mock.mockRejectedValue(new Error('some error'));
200+
201+
const testEvent = {
202+
'detail-type': 'workflow_job',
203+
} as unknown as EventWrapper<WorkflowJobEvent>;
204+
205+
await expect(dispatchToRunners(testEvent, context)).rejects.toThrow();
206+
});
111207
});
112208
});

lambdas/functions/webhook/src/lambda.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import middy from '@middy/core';
22
import { logger, setContext, captureLambdaHandler, tracer } from '@aws-github-runner/aws-powertools-util';
33
import { APIGatewayEvent, Context } from 'aws-lambda';
44

5-
import { handle, publishOnEventBridge, dispatchToRunners } from './webhook';
6-
import { Config } from './ConfigResolver';
5+
import { handle, publishOnEventBridge } from './webhook';
76
import { IncomingHttpHeaders } from 'http';
87
import ValidationError from './ValidationError';
98
import { EventWrapper } from './types';
109
import { WorkflowJobEvent } from '@octokit/webhooks-types';
11-
import { ConfigWebhook, ConfigWebhookEventBridge } from './ConfigLoader';
10+
import { ConfigDispatcher, ConfigWebhook, ConfigWebhookEventBridge } from './ConfigLoader';
11+
import { dispatch } from './runners/dispatch';
1212

1313
export interface Response {
1414
statusCode: number;
@@ -19,13 +19,11 @@ middy(githubWebhook).use(captureLambdaHandler(tracer));
1919

2020
export async function githubWebhook(event: APIGatewayEvent, context: Context): Promise<Response> {
2121
setContext(context, 'lambda.ts');
22-
const config = await Config.load();
23-
2422
logger.logEventIfEnabled(event);
25-
logger.debug('Loading config', { config });
2623

2724
let result: Response;
2825
try {
26+
const config: ConfigWebhook = await ConfigWebhook.load();
2927
result = await handle(headersToLowerCase(event.headers), event.body as string, config);
3028
} catch (e) {
3129
logger.error(`Failed to handle webhook event`, { error: e });
@@ -46,13 +44,11 @@ export async function githubWebhook(event: APIGatewayEvent, context: Context): P
4644

4745
export async function eventBridgeWebhook(event: APIGatewayEvent, context: Context): Promise<Response> {
4846
setContext(context, 'lambda.ts');
49-
const config: ConfigWebhookEventBridge = await ConfigWebhookEventBridge.load();
50-
5147
logger.logEventIfEnabled(event);
52-
logger.debug('Loading config', { config });
5348

5449
let result: Response;
5550
try {
51+
const config: ConfigWebhookEventBridge = await ConfigWebhookEventBridge.load();
5652
result = await publishOnEventBridge(headersToLowerCase(event.headers), event.body as string, config);
5753
} catch (e) {
5854
logger.error(`Failed to handle webhook event`, { error: e });
@@ -71,17 +67,22 @@ export async function eventBridgeWebhook(event: APIGatewayEvent, context: Contex
7167
return result;
7268
}
7369

74-
export async function workflowJob(event: EventWrapper<WorkflowJobEvent>, context: Context): Promise<void> {
70+
export async function dispatchToRunners(event: EventWrapper<WorkflowJobEvent>, context: Context): Promise<void> {
7571
setContext(context, 'lambda.ts');
76-
const config = await Config.load();
77-
7872
logger.logEventIfEnabled(event);
79-
logger.debug('Loading config', { config });
73+
74+
const eventType = event['detail-type'];
75+
if (eventType != 'workflow_job') {
76+
logger.debug('Wrong event type received. Unable to process event', { event });
77+
throw new Error('Incorrect Event detail-type only workflow_job is accepted');
78+
}
8079

8180
try {
82-
await dispatchToRunners(event, config);
81+
const config: ConfigDispatcher = await ConfigDispatcher.load();
82+
await dispatch(event.detail, eventType, config);
8383
} catch (e) {
8484
logger.error(`Failed to handle webhook event`, { error: e });
85+
throw e;
8586
}
8687
}
8788

0 commit comments

Comments
 (0)