Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for in-memory client certificates #952

Merged
merged 9 commits into from
Sep 13, 2024
34 changes: 32 additions & 2 deletions __tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/

import { CliArgs } from '../src/common_types';
import { normalizeOptions } from '../src/options';
import { normalizeOptions, parsePlaywrightOptions } from '../src/options';
import { join } from 'path';

describe('options', () => {
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('options', () => {
ignoreHTTPSErrors: undefined,
isMobile: true,
userAgent:
'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36',
'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36',
viewport: {
height: 658,
width: 320,
Expand Down Expand Up @@ -148,4 +148,34 @@ describe('options', () => {
},
});
});

it('parses cli playwrightOptions.clientCertificates', async () => {
const test = {
clientCertificates: [
{
key: Buffer.from('This should be revived'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for non-buffer values as we discussed.

cert: Buffer.from('This should be revived'),
pfx: Buffer.from('This should be revived'),
origin: Buffer.from('This should not be revived'),
passphrase: Buffer.from('This should not be revived'),
},
{
key: 'This should be revived',
cert: 'This should be revived',
pfx: 'This should be revived',
origin: 'This should not be revived',
passphrase: 'This should not be revived',
},
],
};
const result = parsePlaywrightOptions(JSON.stringify(test));

result.clientCertificates.forEach(t => {
expect(Buffer.isBuffer(t.cert)).toBeTruthy();
expect(Buffer.isBuffer(t.key)).toBeTruthy();
expect(Buffer.isBuffer(t.pfx)).toBeTruthy();
expect(Buffer.isBuffer(t.origin)).toBeFalsy();
expect(Buffer.isBuffer(t.passphrase)).toBeFalsy();
});
});
});
27 changes: 27 additions & 0 deletions __tests__/push/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Push abort on push with clientCertificate.certPath used in cloud 1`] = `
"Aborted. Invalid synthetics project settings.

Certificate path options (certPath, keyPath, pfxPath) are not supported on cloud locations, use in-memory alternatives (cert, key, pfx) when running on cloud.

Run 'npx @elastic/synthetics init' to create project with default settings.
"
`;

exports[`Push abort on push with clientCertificate.keyPath used in cloud 1`] = `
"Aborted. Invalid synthetics project settings.

Certificate path options (certPath, keyPath, pfxPath) are not supported on cloud locations, use in-memory alternatives (cert, key, pfx) when running on cloud.

Run 'npx @elastic/synthetics init' to create project with default settings.
"
`;

exports[`Push abort on push with clientCertificate.pfxPath used in cloud 1`] = `
"Aborted. Invalid synthetics project settings.

Certificate path options (certPath, keyPath, pfxPath) are not supported on cloud locations, use in-memory alternatives (cert, key, pfx) when running on cloud.

Run 'npx @elastic/synthetics init' to create project with default settings.
"
`;

exports[`Push error on empty project id 1`] = `
"Aborted. Invalid synthetics project settings.

Expand Down
53 changes: 38 additions & 15 deletions __tests__/push/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ describe('Push', () => {
) {
await writeFile(
join(PROJECT_DIR, filename),
`export default { monitor: ${JSON.stringify(
monitor
)}, project: ${JSON.stringify(settings)} }`
`export default ${JSON.stringify({ ...settings, monitor })}`
);
}

Expand Down Expand Up @@ -89,20 +87,23 @@ describe('Push', () => {
});

it('error on invalid location', async () => {
await fakeProjectSetup({ id: 'test-project' }, {});
await fakeProjectSetup({ project: { id: 'test-project' } }, {});
const output = await runPush();
expect(output).toMatchSnapshot();
});

it('error when schedule is not present', async () => {
await fakeProjectSetup({ id: 'test-project' }, { locations: ['test-loc'] });
await fakeProjectSetup(
{ project: { id: 'test-project' } },
{ locations: ['test-loc'] }
);
const output = await runPush();
expect(output).toMatchSnapshot();
});

it('error on invalid schedule', async () => {
await fakeProjectSetup(
{ id: 'test-project' },
{ project: { id: 'test-project' } },
{ locations: ['test-loc'], schedule: 12 }
);
const output = await runPush();
Expand All @@ -111,7 +112,7 @@ describe('Push', () => {

it('abort on push with different project id', async () => {
await fakeProjectSetup(
{ id: 'test-project' },
{ project: { id: 'test-project' } },
{ locations: ['test-loc'], schedule: 3 }
);
const output = await runPush(
Expand All @@ -125,7 +126,13 @@ describe('Push', () => {

it('error on invalid schedule in monitor DSL', async () => {
await fakeProjectSetup(
{ id: 'test-project', space: 'dummy', url: 'http://localhost:8080' },
{
project: {
id: 'test-project',
space: 'dummy',
url: 'http://localhost:8080',
},
},
{ locations: ['test-loc'], schedule: 3 }
);
const testJourney = join(PROJECT_DIR, 'test.journey.ts');
Expand All @@ -141,7 +148,7 @@ journey('journey 1', () => monitor.use({ id: 'j1', schedule: 8 }));`

it('errors on duplicate browser monitors', async () => {
await fakeProjectSetup(
{ id: 'test-project', space: 'dummy', url: server.PREFIX },
{ project: { id: 'test-project', space: 'dummy', url: server.PREFIX } },
{ locations: ['test-loc'], schedule: 3 }
);

Expand All @@ -164,7 +171,7 @@ journey('duplicate name', () => monitor.use({ schedule: 15 }));`

it('warn if throttling config is set', async () => {
await fakeProjectSetup(
{ id: 'test-project' },
{ project: { id: 'test-project' } },
{ locations: ['test-loc'], schedule: 3 }
);
const testJourney = join(PROJECT_DIR, 'test.journey.ts');
Expand All @@ -180,7 +187,7 @@ journey('duplicate name', () => monitor.use({ schedule: 15 }));`

it('errors on duplicate lightweight monitors', async () => {
await fakeProjectSetup(
{ id: 'test-project', space: 'dummy', url: server.PREFIX },
{ project: { id: 'test-project', space: 'dummy', url: server.PREFIX } },
{ locations: ['test-loc'], schedule: 3 }
);

Expand Down Expand Up @@ -220,7 +227,7 @@ heartbeat.monitors:

it('error on invalid CHUNK SIZE', async () => {
await fakeProjectSetup(
{ id: 'test-project', space: 'dummy', url: server.PREFIX },
{ project: { id: 'test-project', space: 'dummy', url: server.PREFIX } },
{ locations: ['test-loc'], schedule: 3 }
);
const output = await runPush(undefined, { CHUNK_SIZE: '251' });
Expand All @@ -231,7 +238,7 @@ heartbeat.monitors:

it('respects valid CHUNK SIZE', async () => {
await fakeProjectSetup(
{ id: 'test-project', space: 'dummy', url: server.PREFIX },
{ project: { id: 'test-project', space: 'dummy', url: server.PREFIX } },
{ locations: ['test-loc'], schedule: 3 }
);
const testJourney = join(PROJECT_DIR, 'chunk.journey.ts');
Expand Down Expand Up @@ -260,7 +267,9 @@ heartbeat.monitors:
beforeAll(async () => {
server = await createKibanaTestServer(version);
await fakeProjectSetup(
{ id: 'test-project', space: 'dummy', url: server.PREFIX },
{
project: { id: 'test-project', space: 'dummy', url: server.PREFIX },
},
{ locations: ['test-loc'], schedule: 3 }
);
});
Expand Down Expand Up @@ -314,7 +323,7 @@ heartbeat.monitors:
journey('journey 1', () => monitor.use({ id: 'j1' }));`
);
await fakeProjectSetup(
{ id: 'bar', space: 'dummy', url: server.PREFIX },
{ project: { id: 'bar', space: 'dummy', url: server.PREFIX } },
{ locations: ['test-loc'], schedule: 3 },
'synthetics.config.test.ts'
);
Expand All @@ -330,4 +339,18 @@ heartbeat.monitors:
});
});
});

['certPath', 'keyPath', 'pfxPath'].forEach(key => {
it(`abort on push with clientCertificate.${key} used in cloud`, async () => {
await fakeProjectSetup(
{
project: { id: 'test-project', space: 'dummy', url: server.PREFIX },
playwrightOptions: { clientCertificates: [{ [key]: 'test.file' }] },
},
{ locations: ['test-loc'], schedule: 3 }
);
const output = await runPush();
expect(output).toMatchSnapshot();
});
});
});
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@
"kleur": "^4.1.5",
"micromatch": "^4.0.7",
"pirates": "^4.0.5",
"playwright": "=1.45.1",
"playwright-chromium": "=1.45.1",
"playwright-core": "=1.45.1",
"playwright": "=1.47.0",
"playwright-chromium": "=1.47.0",
"playwright-core": "=1.47.0",
"semver": "^7.5.4",
"sharp": "^0.33.5",
"snakecase-keys": "^4.0.1",
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export async function readConfig(
options = await optionsPromise;
}
}

return options;
}

Expand Down
4 changes: 2 additions & 2 deletions src/formatter/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import {
JavaScriptLanguageGenerator,
JavaScriptFormatter,
} from 'playwright-core/lib/server/recorder/javascript';
} from 'playwright-core/lib/server/codegen/javascript';

export type Step = {
actions: ActionInContext[];
Expand Down Expand Up @@ -272,7 +272,7 @@ export class SyntheticsGenerator extends JavaScriptLanguageGenerator {
if (isAssert && action.command) {
formatter.add(toAssertCall(pageAlias, action));
} else {
formatter.add(super._generateActionCall(subject, action));
formatter.add(super._generateActionCall(subject, actionInContext));
}

if (signals.popup)
Expand Down
44 changes: 42 additions & 2 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,15 @@ export async function normalizeOptions(
*/
const playwrightOpts = merge(
config.playwrightOptions,
cliArgs.playwrightOptions || {}
cliArgs.playwrightOptions || {},
{
arrayMerge(target, source) {
if (source && source.length > 0) {
return [...new Set(source)];
}
return target;
},
}
);
options.playwrightOptions = {
...playwrightOpts,
Expand Down Expand Up @@ -195,7 +203,7 @@ export function getCommonCommandOpts() {
const playwrightOpts = createOption(
'--playwright-options <jsonstring>',
'JSON object to pass in custom Playwright options for the agent. Options passed will be merged with Playwright options defined in your synthetics.config.js file.'
).argParser(JSON.parse);
).argParser(parsePlaywrightOptions);

const pattern = createOption(
'--pattern <pattern>',
Expand Down Expand Up @@ -237,3 +245,35 @@ export function getCommonCommandOpts() {
match,
};
}

export function parsePlaywrightOptions(playwrightOpts: string) {
return JSON.parse(playwrightOpts, (key, value) => {
if (key !== 'clientCertificates') {
return value;
}

// Revive serialized clientCertificates buffer objects
return (value ?? []).map(item => {
const revived = { ...item };
if (item.cert && !Buffer.isBuffer(item.cert)) {
revived.cert = parseAsBuffer(item.cert);
}
if (item.key && !Buffer.isBuffer(item.key)) {
revived.key = parseAsBuffer(item.key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to do the same for pfx option.

}
if (item.pfx && !Buffer.isBuffer(item.pfx)) {
revived.pfx = parseAsBuffer(item.pfx);
}

return revived;
});
});
}

function parseAsBuffer(value: any): Buffer {
try {
return Buffer.from(value);
} catch (e) {
return value;
}
}
Loading