diff --git a/.env.example b/.env.example index bb8bccc..99ad077 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ DEBUG=false SECRET=keyboard-cat NODE_ENV=production REDIS_URL=redis://127.0.0.1:6379 +CONCURRENCY=0 HOST=127.0.0.1 PORT=3000 ENABLE_LEGACY_API=false diff --git a/Dockerfile b/Dockerfile index 5b2707d..fbdde15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,4 +23,4 @@ ENV NODE_ENV=production \ RUN yarn --production ENTRYPOINT ["/usr/bin/tini", "--"] -CMD ["yarn", "start:serve"] +CMD ["yarn", "start:docker"] diff --git a/docker-compose.yml b/docker-compose.yml index 602e9de..7b9293b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,4 +10,4 @@ services: REDIS_URL: redis://redis:6379 redis: - image: redis:alpine + image: redis:7.0-alpine diff --git a/docs/01-installing-tourist.md b/docs/01-installing-tourist.md index 11afd54..6b32178 100644 --- a/docs/01-installing-tourist.md +++ b/docs/01-installing-tourist.md @@ -79,11 +79,18 @@ like [pm2](https://pm2.keymetrics.io/) or with a systemd service. ### Configuration Reference | Option | Default | Description | -| --------------------- | ----------------------- |----------------------------------------------------------------| +|-----------------------|-------------------------|----------------------------------------------------------------| | SECRET | (dynamically generated) | Secret value for token authentication purposes. | | NODE_ENV | production | Environment Tourist is running in. | | REDIS_URL | redis://127.0.0.1:6379 | URL of the redis server. | +| CONCURRENCY | (number of CPU threads) | Maximum number of jobs processed concurrently. | | HOST | 127.0.0.1 | Host address that Tourist will listen on. | | PORT | 3000 | Port on the host address that tourist will listen on. | | ENABLE_LEGACY_API | false | Whether to enable legacy portion of the API (not recommended). | | ENABLE_AUTHENTICATION | true | Whether to enable authentication with tokens (recommended). | + +#### Note on concurrency + +Concurrency value defaults to the number of threads present on the system. It is not recommend to go above this value, +as a headless browser can consume a full thread even for simple operations. You should also account for the RAM +available on your system, as each additional browser can consume somewhere around 100/200MB of RAM. diff --git a/docs/04-using-actions.md b/docs/04-using-actions.md index 694804e..79c4c9e 100644 --- a/docs/04-using-actions.md +++ b/docs/04-using-actions.md @@ -20,70 +20,17 @@ page.locator('#my-button').click(); ``` It gets a little more complicated when trying to click all links, as we need to use the `.all()` method on the locator - -which itself returns a promise. We have two approaches to tackle that: - -1. Use an IIFE statement to be able to await promises: - - ```js - (async () => { - const buttons = await page.locator('button').all(); - for (button of buttons) { - await button.click(); - } - })() - ``` - -2. By using output from other actions - described in the section below: - -### Utilizing the action context - -Outside the `page` and `context` variables - there's one more object available in the sandbox - `actions`. It is an -array of action outputs. After each step's finish - the returned value is assigned to that array, at the same index as -the action that created it. With that in mind we can execute the `locator().all()` chain in the first action, and use -the returned value in the next, image an array of actions like this: - -```js -[ - "page.locator('button').all()", - // the locators are now available under context[0] - `(async () => { - for (const link of actions[0]) { - const pagePromise = context.waitForEvent('page'); - await link.click({ button: 'middle' }); - const newPage = await pagePromise; - await newPage.waitForLoadState(); - await newPage.close(); - } - })()` -] -``` - -While this might not look useful with a simple action like that, we believe that it will be for more complex workloads - -and we want to make Tourist as flexible as possible. - -#### Action output is frozen inside the sandbox - -Keep in mind that it's not possible to attach values to the context on your own on arbitrary indexes: +which itself returns a promise. To accomplish this, use an IIFE statement to be able to await promises: ```js -actions[100] = "test" -``` - -Action like this will have a different result than you might expect. The array itself is frozen and can't be modified -inside the sandbox - however the assignment operation returns the assigned value - so the value `"test"` will be -available on whatever index the action had. - -This is by design - we don't want to prevent from assigning arbitrary values you might consider useful to the output, -we just want to ensure that they are placed on appropriate indexes and don't override each-other. - -Security is provided by disallowing code generation inside the sandbox: - -```js -new Function('return (1+2)')() +(async () => { + const buttons = await page.locator('button').all(); + for (button of buttons) { + await button.click(); + } +})() ``` -Defining such action will raise an exception, and prevent further execution. - ### Internals of the PlaywrightRunner Actions are executed by `PlaywrightRunner` instances, created for each job. Let's quickly go over the livecycle of the @@ -93,17 +40,15 @@ playwright runner: 2. Cookies are attached to the playwright context, page and context are created. 3. Actions are split into `preOpen` and `postOpen`. That's because some actions need to be executed before the page is visited - like the `page.on` event handler. -4. A new VM is created, and the playwright page, context as well as the `actions` output array are frozen inside. +4. A new VM is created, and the playwright page and context are frozen inside. 5. `preOpen` actions are executed before navigation. 6. Navigation happens, the runner waits for the page to load, and executes `postOpen` actions inside the vm. **It's important to understand that the `VM.run()` method is synchronous, that's why wrapping async code with iife expressions is necessary.** It may of course, return a Promise, which is still a synchronous operation, and the runner will await that Promise in its own async context, or just move forward if a concrete value has been returned. -7. Once the Promise is fulfilled, its value is assigned to the action output, at the index of the action that returned -it. -8. After the action is fully completed, the runner waits for the page to be in the load state again, and repeats the +7. After the action is fully completed, the runner waits for the page to be in the load state again, and repeats the process for all the actions. -9. Finally, the runner moves onto finish and teardown. It gathers requested files: recording / screenshot / pdf, and +8. Finally, the runner moves onto finish and teardown. It gathers requested files: recording / screenshot / pdf, and closes the browser, the context and the page. #### A word about .map, .forEach and .reduce @@ -112,7 +57,7 @@ This is not an issue specific to Tourist, however it's important to understand h mentioned methods. It might be tempting to create an action like this: ```js -actions[0].map(async link => { +links.map(async link => { await link.click({ button: "middle" }); }) ``` @@ -121,7 +66,7 @@ However, **this will not work**, because playwright will attempt to click all th As the async function returns a Promise when it's executed - it will not block further execution because there's nothing awaiting it. This code will evaluate to an array of ready, but not fulfilled Promises. That's because Tourist will await the full context - but not individual promises. Wrapping this code inside `Promise.all()` will only solve one of -the issues - the Promises will not be fulfilled, however playwright will still fail to click all the links. +the issues - the Promises will be fulfilled, however playwright will still fail to click all the links. The correct way to approach this is to use a for loop inside an iife expression, as well as waiting for the page to load by using the playwright context: diff --git a/package.json b/package.json index 1b534da..19b90c7 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ "license": "Apache-2.0", "scripts": { "cmd:get-issuer-token": "ts-node ./src/cli.ts get-issuer-token", - "build": "rm -rf ./out && tsc -p tsconfig.json", - "start:serve": "node ./src/index.js", - "start:build": "yarn build && node out/index.js", - "start:app": "nodemon ./src/index.ts | pino-pretty", + "build": "rm -rf ./out && tsc -p tsconfig.production.json", + "start:docker": "node ./src/index.js | pino-pretty", + "start:serve": "node ./out/index.js | pino-pretty", + "start:build": "yarn build && yarn start:serve", + "start:dev": "nodemon ./src/index.ts | pino-pretty", "start:test-app": "nodemon ./tests/utils/_server.ts", "format:code": "prettier '{src,tests}/**/*.ts' --write", "lint:code": "prettier --check '{src,tests}/**/*.ts'", @@ -28,13 +29,15 @@ "@fastify/swagger": "^8.2.1", "@fastify/swagger-ui": "^1.3.0", "@fastify/type-provider-typebox": "^2.4.0", - "@immobiliarelabs/fastify-sentry": "^5.0.1", + "@sentry/integrations": "^7.46.0", + "@sentry/node": "^7.46.0", "@sinclair/typebox": "^0.25.24", "bull": "^4.10.2", "dotenv": "^16.0.3", "fastify": "^4.13.0", "jsonwebtoken": "^9.0.0", "lodash": "^4.17.21", + "pino-pretty": "^9.1.1", "playwright": "^1.31.1", "vm2": "^3.9.13" }, @@ -49,7 +52,6 @@ "c8": "^7.12.0", "markdownlint-cli": "^0.33.0", "nodemon": "^2.0.20", - "pino-pretty": "^9.1.1", "prettier": "^2.8.4", "ts-node": "^10.9.1", "typescript": "^4.9.4", @@ -66,9 +68,6 @@ "nodeArguments": [ "--loader=ts-node/esm", "--no-warnings" - ], - "ignoredByWatcher": [ - "static/openapi.json" ] } } diff --git a/src/app.ts b/src/app.ts index 8397fe8..0aa77c4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,10 +1,10 @@ import Fastify, { FastifyServerOptions } from "fastify"; import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox"; -import fastifySentry from "@immobiliarelabs/fastify-sentry"; import fastifySwagger from "@fastify/swagger"; import fastifySwaggerUI from "@fastify/swagger-ui"; +import initSentry from "./sentry"; import { TouristConfig } from "./config"; import { createRouter } from "./router"; import { SwaggerConfig } from "./swagger"; @@ -18,10 +18,8 @@ export const createApp = async ( app.decorate("config", config); if (app.config.SENTRY_DSN) { - app.register(fastifySentry, { - dsn: app.config.SENTRY_DSN, - environment: app.config.ENV, - }); + app.log.info("Sentry Reporting Enabled"); + initSentry(app); } app.register(fastifySwagger, SwaggerConfig); diff --git a/src/config.ts b/src/config.ts index c249d48..4433cea 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,14 @@ +import os from "os"; import path from "path"; import dotenv from "dotenv"; import crypto from "crypto"; +import _ from "lodash"; import { parseBool } from "./utils/config"; export declare type TouristConfig = { DEBUG: boolean; + CONCURRENCY: number; SECRET: string; ENV: string; REDIS_URL: string; @@ -20,17 +23,26 @@ if (process.env.NODE_ENV !== "test") { dotenv.config({ path: path.resolve(path.join(__dirname, "..", ".env")) }); } -export default { - DEBUG: parseBool(process.env.DEBUG, false), - SECRET: process.env.SECRET || crypto.randomBytes(48).toString("hex"), - ENV: process.env.NODE_ENV || "production", - REDIS_URL: process.env.REDIS_URL || "redis://127.0.0.1:6379", - HOST: process.env.HOST || "127.0.0.1", - PORT: parseInt(process.env.PORT ? process.env.PORT : "3000"), - ENABLE_LEGACY_API: parseBool(process.env.ENABLE_LEGACY_API, false), - ENABLE_AUTHENTICATION: parseBool(process.env.ENABLE_AUTHENTICATION, true), - SENTRY_DSN: - process.env.SENTRY_DSN && process.env.SENTRY_DSN !== "" - ? process.env.SENTRY_DSN - : false, -} as TouristConfig; +const getConfig = () => + ({ + DEBUG: parseBool(process.env.DEBUG, false), + SECRET: process.env.SECRET || crypto.randomBytes(48).toString("hex"), + ENV: process.env.NODE_ENV || "production", + REDIS_URL: process.env.REDIS_URL || "redis://127.0.0.1:6379", + CONCURRENCY: + process.env.CONCURRENCY && + process.env.CONCURRENCY !== "" && + process.env.CONCURRENCY !== "0" + ? parseInt(process.env.CONCURRENCY) + : os.cpus().length, + HOST: process.env.HOST || "127.0.0.1", + PORT: parseInt(process.env.PORT ? process.env.PORT : "3000"), + ENABLE_LEGACY_API: parseBool(process.env.ENABLE_LEGACY_API, false), + ENABLE_AUTHENTICATION: parseBool(process.env.ENABLE_AUTHENTICATION, true), + SENTRY_DSN: + process.env.SENTRY_DSN && process.env.SENTRY_DSN !== "" + ? process.env.SENTRY_DSN + : false, + } as TouristConfig); + +export default _.memoize(getConfig)(); diff --git a/src/jobs/api.ts b/src/jobs/api.ts index 92e534e..b992be3 100644 --- a/src/jobs/api.ts +++ b/src/jobs/api.ts @@ -1,4 +1,5 @@ import { Job } from "bull"; +import * as Sentry from "@sentry/node"; import config from "../config"; import { JobBrowser, JobCookieType, JobOptions, JobStepType } from "../schemas/api"; @@ -13,12 +14,17 @@ export declare type VisitJobData = { export const asyncVisitJob = async (job: Job) => { const { data } = job; + const runner = new PlaywrightRunner(data, config.DEBUG); try { await runner.init(); await runner.exec(); } catch (e: any) { + if (config.SENTRY_DSN) { + Sentry.captureException(e); + } + // change the job status to failed with the error message await job.moveToFailed({ message: e.message }); } diff --git a/src/queue.ts b/src/queue.ts index 71dbe9c..5b98fe6 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -2,6 +2,8 @@ import Queue from "bull"; import { legacySimpleVisitJob, LegacySimpleVisitJobData } from "./jobs/legacy"; import { asyncVisitJob, VisitJobData } from "./jobs/api"; +import config from "./config"; + const LegacySimpleVisitQueue = new Queue( "Legacy Simple Visit Job", process.env.REDIS_URL || "redis://127.0.0.1:6379", @@ -13,7 +15,7 @@ const AsyncVisitQueue = new Queue( ); // Assign worker functions to queues -LegacySimpleVisitQueue.process(legacySimpleVisitJob); -AsyncVisitQueue.process(asyncVisitJob); +LegacySimpleVisitQueue.process(config.CONCURRENCY, legacySimpleVisitJob); +AsyncVisitQueue.process(config.CONCURRENCY, asyncVisitJob); export { LegacySimpleVisitQueue, AsyncVisitQueue }; diff --git a/src/routes/api.ts b/src/routes/api.ts index 20d9316..023eb9f 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -4,8 +4,8 @@ import { FastifyReply, FastifyRequest, } from "fastify"; - import _ from "lodash"; +import * as Sentry from "@sentry/node"; import { JobDispatchRequest, @@ -31,6 +31,14 @@ import { AsyncJob403Reply, SyncJob401Reply, SyncJob403Reply, + SyncJob401ReplyType, + SyncJob403ReplyType, + AsyncJob403ReplyType, + AsyncJob401ReplyType, + AsyncJobStatus401Reply, + AsyncJobStatus403Reply, + AsyncJobStatus401ReplyType, + AsyncJobStatus403ReplyType, } from "../schemas/api"; import { AsyncVisitQueue } from "../queue"; import { syncVisitJob, VisitJobData } from "../jobs/api"; @@ -44,7 +52,11 @@ export default ( fastify.post<{ Headers: JobDispatchRequestHeadersType; Body: JobDispatchRequestType; - Reply: AsyncJob200ReplyType | AsyncJob400ReplyType; + Reply: + | AsyncJob200ReplyType + | AsyncJob400ReplyType + | AsyncJob401ReplyType + | AsyncJob403ReplyType; }>("/async-job", { schema: { headers: JobDispatchRequestHeaders, @@ -62,7 +74,11 @@ export default ( fastify.post<{ Headers: JobDispatchRequestHeadersType; Body: JobDispatchRequestType; - Reply: SyncJob200ReplyType | SyncJob400ReplyType; + Reply: + | SyncJob200ReplyType + | SyncJob400ReplyType + | SyncJob401ReplyType + | SyncJob403ReplyType; }>("/sync-job", { schema: { body: JobDispatchRequest, @@ -78,12 +94,18 @@ export default ( fastify.get<{ Querystring: AsyncJobStatusRequestType; - Reply: AsyncJobStatus200ReplyType | AsyncJobStatus404ReplyType; + Reply: + | AsyncJobStatus200ReplyType + | AsyncJobStatus401ReplyType + | AsyncJobStatus403ReplyType + | AsyncJobStatus404ReplyType; }>("/job-status", { schema: { querystring: AsyncJobStatusRequest, response: { 200: AsyncJobStatus200Reply, + 401: AsyncJobStatus401Reply, + 403: AsyncJobStatus403Reply, 404: AsyncJobStatus404Reply, }, }, @@ -93,7 +115,7 @@ export default ( done(); }; -const authenticateDispatchRequest = (request: FastifyRequest, secret: string) => { +const authenticateDispatchRequest = (request: FastifyRequest) => { const data = request.body; const { authorization } = request.headers; @@ -106,7 +128,7 @@ const authenticateDispatchRequest = (request: FastifyRequest, secret: string) => } const visitURLs = _.map(data.steps, "url"); - if (!authenticateVisitToken(authorization, visitURLs, secret)) { + if (!authenticateVisitToken(authorization, visitURLs)) { return { statusCode: 403, error: "Forbidden", @@ -118,11 +140,7 @@ const authenticateDispatchRequest = (request: FastifyRequest, secret: string) => return true; }; -const authenticateStatusRequest = ( - request: FastifyRequest, - data: VisitJobData, - secret: string, -) => { +const authenticateStatusRequest = (request: FastifyRequest, data: VisitJobData) => { const { authorization } = request.headers; if (!authorization) { @@ -134,7 +152,7 @@ const authenticateStatusRequest = ( } const visitURLs = _.map(data.steps, "url"); - if (!authenticateVisitToken(authorization, visitURLs, secret)) { + if (!authenticateVisitToken(authorization, visitURLs)) { return { statusCode: 403, error: "Forbidden", @@ -150,10 +168,7 @@ const getAsyncJobHandler = (fastify: FastifyInstance) => { const data = request.body; if (fastify.config.ENABLE_AUTHENTICATION) { - const authenticationResult = authenticateDispatchRequest( - request, - fastify.config.SECRET, - ); + const authenticationResult = authenticateDispatchRequest(request); if (authenticationResult !== true) { return reply.status(authenticationResult.statusCode).send(authenticationResult); @@ -170,17 +185,25 @@ const getSyncJobHandler = (fastify: FastifyInstance) => { const data = request.body; if (fastify.config.ENABLE_AUTHENTICATION) { - const authenticationResult = authenticateDispatchRequest( - request, - fastify.config.SECRET, - ); + const authenticationResult = authenticateDispatchRequest(request); + if (authenticationResult !== true) { return reply.status(authenticationResult.statusCode).send(authenticationResult); } } - const jobResult = await syncVisitJob(data); - return reply.send({ status: "success", result: jobResult }); + try { + const jobResult = await syncVisitJob(data); + return reply.send({ status: "success", result: jobResult }); + } catch (e: any) { + if (fastify.config.SENTRY_DSN) { + Sentry.captureException(e); + } + + return reply + .status(400) + .send({ statusCode: 400, error: "Bad Request", message: e.message }); + } }; }; @@ -198,11 +221,7 @@ const getAsyncJobStatusHandler = (fastify: FastifyInstance) => { } if (fastify.config.ENABLE_AUTHENTICATION) { - const authenticationResult = authenticateStatusRequest( - request, - job.data, - fastify.config.SECRET, - ); + const authenticationResult = authenticateStatusRequest(request, job.data); if (authenticationResult !== true) { return reply.status(authenticationResult.statusCode).send(authenticationResult); diff --git a/src/routes/auth.ts b/src/routes/auth.ts index fa46628..4ac64e1 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -7,6 +7,7 @@ import { import jwt from "jsonwebtoken"; +import config from "../config"; import { IssueToken200Reply, IssueToken200ReplyType, @@ -47,7 +48,7 @@ const getIssueTokenHandler = (fastify: FastifyInstance) => { const { authorization } = request.headers; const { validity, scope, strict } = request.body; - if (!authenticateIssuerToken(authorization, fastify.config.SECRET)) { + if (!authenticateIssuerToken(authorization)) { return reply.status(401).send({ statusCode: 401, error: "Invalid Issuer Token", @@ -60,7 +61,7 @@ const getIssueTokenHandler = (fastify: FastifyInstance) => { strict, }; - const token = jwt.sign(payload, fastify.config.SECRET, { expiresIn: validity }); + const token = jwt.sign(payload, config.SECRET, { expiresIn: validity }); reply.send({ token }); }; }; diff --git a/src/schemas/api.ts b/src/schemas/api.ts index 885d163..8327ee7 100644 --- a/src/schemas/api.ts +++ b/src/schemas/api.ts @@ -120,6 +120,8 @@ export const AsyncJob401Reply = Type.Object({ message: Type.String(), }); +export type AsyncJob401ReplyType = Static; + export const AsyncJob403Reply = Type.Object({ statusCode: Type.Literal(403), error: Type.String(), @@ -145,6 +147,22 @@ export const AsyncJobStatus200Reply = Type.Object({ export type AsyncJobStatus200ReplyType = Static; +export const AsyncJobStatus401Reply = Type.Object({ + statusCode: Type.Literal(401), + error: Type.String(), + message: Type.String(), +}); + +export type AsyncJobStatus401ReplyType = Static; + +export const AsyncJobStatus403Reply = Type.Object({ + statusCode: Type.Literal(403), + error: Type.String(), + message: Type.String(), +}); + +export type AsyncJobStatus403ReplyType = Static; + export const AsyncJobStatus404Reply = Type.Object({ statusCode: Type.Literal(404), message: Type.String(), diff --git a/src/sentry.ts b/src/sentry.ts new file mode 100644 index 0000000..9490dec --- /dev/null +++ b/src/sentry.ts @@ -0,0 +1,29 @@ +import { FastifyInstance } from "fastify"; + +import * as Sentry from "@sentry/node"; +import { RewriteFrames } from "@sentry/integrations"; + +// Sentry Typescript Integration: https://docs.sentry.io/platforms/node/typescript/ +global.__rootdir__ = __dirname || process.cwd(); + +declare global { + var __rootdir__: string; +} + +export default (app: FastifyInstance) => { + Sentry.init({ + // @ts-ignore: SENTRY_DSN has to be defined for this to load + dsn: app.config.SENTRY_DSN, + tracesSampleRate: 1.0, + integrations: [ + new RewriteFrames({ + root: global.__rootdir__, + }), + ], + }); + + app.setErrorHandler(async (error, request, reply) => { + Sentry.captureException(error); + return reply.status(500).send({ error: "Internal Server Error" }); + }); +}; diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 0ab6945..1e72094 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,6 +1,8 @@ import url from "url"; import jwt from "jsonwebtoken"; +import config from "../config"; + export const getIssuerToken = (secret: string): string => { const payload = { master: true, @@ -22,8 +24,8 @@ export const extractToken = (header: string): string | false => { return parts[1]; }; -export const authenticateIssuerToken = (header: string, secret: string): boolean => { - if (secret.trim() === "") { +export const authenticateIssuerToken = (header: string): boolean => { + if (config.SECRET.trim() === "") { return false; } @@ -34,7 +36,7 @@ export const authenticateIssuerToken = (header: string, secret: string): boolean let payload: string | jwt.JwtPayload; try { - payload = jwt.verify(token, secret); + payload = jwt.verify(token, config.SECRET); } catch (e) { // token is invalid return false; @@ -63,15 +65,15 @@ export const getBaseHost = (u: string): string | null => { if (parts.length > 2) { return parts.slice(parts.length - 2).join("."); } + return hostname; }; export const authenticateVisitToken = ( header: string, visitURLs: string[], - secret: string, ): boolean => { - if (secret.trim() === "") { + if (config.SECRET.trim() === "") { return false; } @@ -82,7 +84,7 @@ export const authenticateVisitToken = ( let payload: string | jwt.JwtPayload; try { - payload = jwt.verify(token, secret); + payload = jwt.verify(token, config.SECRET); } catch (e) { return false; } diff --git a/src/utils/runner.ts b/src/utils/runner.ts index 41f03d9..d41a794 100644 --- a/src/utils/runner.ts +++ b/src/utils/runner.ts @@ -30,7 +30,6 @@ export class PlaywrightRunner { public readonly steps: JobStepType[]; public readonly cookies: JobCookieType[]; public options: JobOptions[] = []; - private _actionContext: Array = []; private readonly _debug: boolean; constructor(data: PlaywrightRunnerData, debug: boolean = false) { @@ -111,10 +110,9 @@ export class PlaywrightRunner { wasm: false, }); - const { page, context, _actionContext } = this; + const { page, context } = this; vm.freeze(page, "page"); vm.freeze(context, "context"); - vm.freeze(_actionContext, "actions"); if (this._debug) { vm.freeze(console, "console"); @@ -124,8 +122,7 @@ export class PlaywrightRunner { if (step.actions && actions.preOpen) { for (const preOpenAction of actions.preOpen) { try { - const idx = step.actions.indexOf(preOpenAction); - this._actionContext[idx] = await vm.run(preOpenAction); + await vm.run(preOpenAction); } catch (e: any) { const msg = e.message ? ` - ${e.message}` : ""; await this.teardown(); @@ -141,8 +138,7 @@ export class PlaywrightRunner { if (step.actions && actions.postOpen) { for (const postOpenAction of actions.postOpen) { try { - const idx = step.actions.indexOf(postOpenAction); - this._actionContext[idx] = await vm.run(postOpenAction); + await vm.run(postOpenAction); await this.page.waitForLoadState(); } catch (e: any) { const msg = e.message ? ` - ${e.message}` : ""; @@ -193,8 +189,6 @@ export class PlaywrightRunner { // teardown() removes the browser and context from the instance as well as closes them public async teardown() { - this._actionContext = []; - if (this.context) { await this.context.close(); delete this.context; @@ -207,9 +201,4 @@ export class PlaywrightRunner { this.browser = undefined; } } - - // helper getter for the private _actionContext for peeking at the context during tests - get actionContext() { - return this._actionContext; - } } diff --git a/tests/auth.test.ts b/tests/auth.test.ts index 8512507..0b96f33 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -2,6 +2,7 @@ import anyTest, { TestFn } from "ava"; import { FastifyInstance } from "fastify"; import jwt from "jsonwebtoken"; +import config from "../src/config"; import { createApp } from "../src/app"; import { AsyncVisitQueue } from "../src/queue"; import { getIssuerToken } from "../src/utils/auth"; @@ -17,7 +18,7 @@ import { timestamp, } from "./utils/_common"; -const TEST_SECRET = "keyboard-cat"; +const TEST_SECRET = config.SECRET; const TEST_TOKEN = jwt.sign( { scope: "https://example.com", diff --git a/tests/auth.utils.test.ts b/tests/auth.utils.test.ts index 00cc125..36d1f68 100644 --- a/tests/auth.utils.test.ts +++ b/tests/auth.utils.test.ts @@ -1,6 +1,8 @@ import test from "ava"; import jwt from "jsonwebtoken"; +import config from "../src/config"; + import { authenticateIssuerToken, authenticateVisitToken, @@ -12,7 +14,7 @@ import { // @ts-ignore: tests directory is not under rootDir, because we're using ts-node for testing import { timestamp } from "./utils/_common"; -const TEST_SECRET = "keyboard-cat"; +const TEST_SECRET = config.SECRET; const TEST_TOKEN = jwt.sign( { scope: "https://example.com", @@ -65,32 +67,19 @@ test("extractToken returns false with incorrectly formatted header", async (t) = }); test("authenticateIssuerToken returns true if token is a valid issuer token", async (t) => { - t.is(authenticateIssuerToken(`Bearer ${TEST_ISSUER_TOKEN}`, TEST_SECRET), true); + t.is(authenticateIssuerToken(`Bearer ${TEST_ISSUER_TOKEN}`), true); }); test("authenticateIssuerToken returns false if token could not be extracted", async (t) => { - t.is(authenticateIssuerToken(`Bearer`, TEST_SECRET), false); + t.is(authenticateIssuerToken(`Bearer`), false); }); -test("authenticateIssuerToken returns false if secret was not provided", async (t) => { - t.is(authenticateIssuerToken(`Bearer ${TEST_ISSUER_TOKEN}`, ""), false); - t.is(authenticateIssuerToken(`Bearer ${TEST_ISSUER_TOKEN}`, " "), false); - t.is(authenticateIssuerToken(`Bearer ${TEST_ISSUER_TOKEN}`, " "), false); -}); - -test("authenticateIssuerToken returns false if token or secret are invalid", async (t) => { - t.is( - authenticateIssuerToken( - `Bearer eyfpdskfds.invalid-token.sadoiadhasio`, - TEST_SECRET, - ), - false, - ); - t.is(authenticateIssuerToken(`Bearer ${TEST_ISSUER_TOKEN}`, "invalid-secret"), false); +test("authenticateIssuerToken returns false if token is invalid", async (t) => { + t.is(authenticateIssuerToken(`Bearer eyfpdskfds.invalid-token.sadoiadhasio`), false); }); test("authenticateIssuerToken returns false if token is not an issuer token", async (t) => { - t.is(authenticateIssuerToken(`Bearer ${TEST_TOKEN}`, TEST_SECRET), false); + t.is(authenticateIssuerToken(`Bearer ${TEST_TOKEN}`), false); }); test("getBaseHost returns the base host of a given URL", async (t) => { @@ -123,208 +112,115 @@ test("getBaseHost returns the base host of a given URL", async (t) => { test("authenticateVisitToken returns true if token is a valid visit token for given URLs", async (t) => { // non-strict token + t.is(authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com"]), true); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com:8443"]), true, ); + t.is(authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["http://example.com"]), true); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com:8443"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["http://example.com:8000"]), true, ); t.is( - authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["http://example.com"], TEST_SECRET), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://test.example.com"]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["http://example.com:8000"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://test.test.example.com"]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://test.example.com:8443"]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.test.example.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.test.example.com:8443", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com:8443"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com/test"]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.test.example.com:8443"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com:8443/test"]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://test.example.com/test"]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com:8443/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.example.com:8443/test", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com?test=test"]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com:8443/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://example.com:8443?test=test", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://example.com/test?test=test", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com:8443?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://example.com:8443/test?test=test", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com/test?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.example.com?test=test", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com:8443/test?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.example.com:8443?test=test", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.example.com/test?test=test", + ]), true, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com:8443?test=test"], - TEST_SECRET, - ), - true, - ); - t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com/test?test=test"], - TEST_SECRET, - ), - true, - ); - t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example.com:8443/test?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.example.com:8443/test?test=test", + ]), true, ); // strict token t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://example.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, ["https://example.com"]), true, ); }); test("authenticateVisitToken returns false if token could not be extracted", async (t) => { - t.is(authenticateVisitToken(`Bearer`, ["https://example.com"], TEST_SECRET), false); -}); - -test("authenticateVisitToken returns false if secret was not provided", async (t) => { - t.is( - authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com"], ""), - false, - ); - t.is( - authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com"], " "), - false, - ); - t.is( - authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example.com"], " "), - false, - ); + t.is(authenticateVisitToken(`Bearer`, ["https://example.com"]), false); }); -test("authenticateVisitToken returns false if token or secret are invalid", async (t) => { - t.is( - authenticateVisitToken( - `Bearer eyfpdskfds.invalid-token.sadoiadhasio`, - ["https://example.com"], - TEST_SECRET, - ), - false, - ); +test("authenticateVisitToken returns false if token is invalid", async (t) => { t.is( - authenticateVisitToken( - `Bearer ${TEST_ISSUER_TOKEN}`, - ["https://example.com"], - "invalid-secret", - ), + authenticateVisitToken(`Bearer eyfpdskfds.invalid-token.sadoiadhasio`, [ + "https://example.com", + ]), false, ); }); @@ -332,159 +228,112 @@ test("authenticateVisitToken returns false if token or secret are invalid", asyn test("authenticateVisitToken returns false if token is not valid for all given URLs", async (t) => { // non-strict token t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com", "https://example1.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://example.com", + "https://example1.com", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example.com", "http://example1.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://example.com", + "http://example1.com", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example1.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://test.example1.com"]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example1.com/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example1.com/test"]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example1.com/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://test.example1.com/test"]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example1.com?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, ["https://example1.com?test=test"]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://example1.com/test?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://example1.com/test?test=test", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example1.com?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.example1.com?test=test", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_TOKEN}`, - ["https://test.example1.com/test?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_TOKEN}`, [ + "https://test.example1.com/test?test=test", + ]), false, ); // strict token t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://example.com", "http://example.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, [ + "https://example.com", + "http://example.com", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://test.example.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, ["https://test.example.com"]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://example.com/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, ["https://example.com/test"]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://test.example.com/test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, [ + "https://test.example.com/test", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://example.com?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, [ + "https://example.com?test=test", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://example.com/test?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, [ + "https://example.com/test?test=test", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://test.example.com?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, [ + "https://test.example.com?test=test", + ]), false, ); t.is( - authenticateVisitToken( - `Bearer ${TEST_STRICT_TOKEN}`, - ["https://test.example.com/test?test=test"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_STRICT_TOKEN}`, [ + "https://test.example.com/test?test=test", + ]), false, ); }); test("authenticateVisitToken returns false if token is expired", async (t) => { t.is( - authenticateVisitToken( - `Bearer ${TEST_EXPIRED_TOKEN}`, - ["https://example.com"], - TEST_SECRET, - ), + authenticateVisitToken(`Bearer ${TEST_EXPIRED_TOKEN}`, ["https://example.com"]), false, ); }); test("authenticateVisitToken returns false if token payload is not an object", async (t) => { const token = jwt.sign("test", TEST_SECRET); - t.is(authenticateVisitToken(`Bearer ${token}`, [], TEST_SECRET), false); + t.is(authenticateVisitToken(`Bearer ${token}`, []), false); }); test("getIssuerToken returns a correct issuer token", async (t) => { diff --git a/tests/config.test.ts b/tests/config.test.ts index 39d3fdc..9077aee 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -1,9 +1,12 @@ +import os from "os"; import test from "ava"; import config from "../src/config"; test("config has correct default values", async (t) => { // ava will automatically set NODE_ENV to "test" t.is(config.ENV, "test"); + t.assert(typeof config.SECRET === "string"); + t.is(config.CONCURRENCY, os.cpus().length); t.is(config.DEBUG, false); t.is(config.REDIS_URL, "redis://127.0.0.1:6379"); t.is(config.HOST, "127.0.0.1"); diff --git a/tests/runner.test.ts b/tests/runner.test.ts index dc1f2a3..e5cece1 100644 --- a/tests/runner.test.ts +++ b/tests/runner.test.ts @@ -268,135 +268,6 @@ test("PlaywrightRunner steps can interact with anchors", async (t) => { t.is(headers.referer, `${testAppURL}/anchor?id=${testID}`); }); -test("PlaywrightRunner actions can use output from the actions output (for-async)", async (t) => { - const { testApp, testAppURL } = t.context; - const testID = uuid4(); - - const runner = new PlaywrightRunner({ - browser: JobBrowser.CHROMIUM, - steps: [ - { - url: `${testAppURL}/multiple-anchors?id=${testID}`, - actions: [ - "page.locator('a').all()", - `(async () => { - for (const link of actions[0]) { - await link.click({ button: "middle" }); - } - })()`, - ], - }, - ], - cookies: [], - options: [], - }); - - await runner.init(); - await runner.exec(); - await runner.finish(); - - for (let i = 1; i <= 3; i++) { - const inspection = await testApp.inject({ - method: "GET", - url: "/inspect-req", - query: { - id: `${testID}-${i}`, - }, - }); - - const { headers } = inspection.json(); - t.assert(headers.hasOwnProperty("referer")); - t.is(headers.referer, `${testAppURL}/multiple-anchors?id=${testID}`); - } -}); - -test("PlaywrightRunner actions can use output from the actions output (reduce-promise)", async (t) => { - const { testApp, testAppURL } = t.context; - const testID = uuid4(); - - const runner = new PlaywrightRunner({ - browser: JobBrowser.CHROMIUM, - steps: [ - { - url: `${testAppURL}/multiple-anchors?id=${testID}`, - actions: [ - "page.locator('a').all()", - `actions[0].reduce( - (previous, link) => previous.then( - () => link.click({ button: "middle" }).then(null) - ), - Promise.resolve(null) - );`, - ], - }, - ], - cookies: [], - options: [], - }); - - await runner.init(); - await runner.exec(); - await runner.finish(); - - for (let i = 1; i <= 3; i++) { - const inspection = await testApp.inject({ - method: "GET", - url: "/inspect-req", - query: { - id: `${testID}-${i}`, - }, - }); - - const { headers } = inspection.json(); - t.assert(headers.hasOwnProperty("referer")); - t.is(headers.referer, `${testAppURL}/multiple-anchors?id=${testID}`); - } -}); - -test("PlaywrightRunner actions can use output from the actions output (reduce-async)", async (t) => { - const { testApp, testAppURL } = t.context; - const testID = uuid4(); - - const runner = new PlaywrightRunner({ - browser: JobBrowser.CHROMIUM, - steps: [ - { - url: `${testAppURL}/multiple-anchors?id=${testID}`, - actions: [ - "page.locator('a').all()", - `actions[0].reduce( - async (previous, link) => { - await previous; - await link.click({ button: "middle" }); - }, - Promise.resolve(null) - );`, - ], - }, - ], - cookies: [], - options: [], - }); - - await runner.init(); - await runner.exec(); - await runner.finish(); - - for (let i = 1; i <= 3; i++) { - const inspection = await testApp.inject({ - method: "GET", - url: "/inspect-req", - query: { - id: `${testID}-${i}`, - }, - }); - - const { headers } = inspection.json(); - t.assert(headers.hasOwnProperty("referer")); - t.is(headers.referer, `${testAppURL}/multiple-anchors?id=${testID}`); - } -}); - test("PlaywrightRunner actions can use playwright context", async (t) => { const { testApp, testAppURL } = t.context; const testID = uuid4(); @@ -443,49 +314,6 @@ test("PlaywrightRunner actions can use playwright context", async (t) => { } }); -test("PlaywrightRunner assigns correct index to output values in the actions output", async (t) => { - const { testApp, testAppURL } = t.context; - const testID = uuid4(); - - const runner = new PlaywrightRunner({ - browser: JobBrowser.CHROMIUM, - steps: [ - { - url: `${testAppURL}/anchor?id=${testID}`, - actions: [ - // output should still be available under actions[0] - // even though page.on will be moved before that - "page.locator('a').all()", - "page.on('dialog', dialog => dialog.accept())", - `(async () => { - for (const link of actions[0]) { - await link.click({ button: "middle" }); - } - })()`, - ], - }, - ], - cookies: [], - options: [], - }); - - await runner.init(); - await runner.exec(); - await runner.finish(); - - const inspection = await testApp.inject({ - method: "GET", - url: "/inspect-req", - query: { - id: `${testID}`, - }, - }); - - const { headers } = inspection.json(); - t.assert(headers.hasOwnProperty("referer")); - t.is(headers.referer, `${testAppURL}/anchor?id=${testID}`); -}); - test("PlaywrightRunner actions cannot generate code from strings", async (t) => { const { testAppURL } = t.context; const testID = uuid4(); @@ -506,9 +334,6 @@ test("PlaywrightRunner actions cannot generate code from strings", async (t) => async () => { await runner_1.init(); await runner_1.exec(); - // code generation should not trigger, and the step should not complete - // so the expected value here is undefined (no assignment has been made) - t.assert(typeof runner_1.actionContext[0] === "undefined"); await runner_1.finish(); }, { @@ -536,9 +361,6 @@ test("PlaywrightRunner actions cannot generate code from strings", async (t) => async () => { await runner_2.init(); await runner_2.exec(); - // code generation should not trigger, and the step should not complete - // so the expected value here is undefined (no assignment has been made) - t.assert(typeof runner_2.actionContext[0] === "undefined"); await runner_2.finish(); }, { @@ -557,67 +379,6 @@ test("PlaywrightRunner actions cannot generate code from strings", async (t) => ); }); -test("PlaywrightRunner actions cannot assign properties to frozen objects", async (t) => { - const { testAppURL } = t.context; - const testID = uuid4(); - - const runner_1 = new PlaywrightRunner({ - browser: JobBrowser.CHROMIUM, - steps: [ - { - url: `${testAppURL}/anchor?id=${testID}`, - actions: ["page.test = 'test'"], - }, - ], - cookies: [], - options: [], - }); - - await runner_1.init(); - await runner_1.exec(); - t.false(runner_1.page!.hasOwnProperty("test")); - await runner_1.finish(); - - const runner_2 = new PlaywrightRunner({ - browser: JobBrowser.CHROMIUM, - steps: [ - { - url: `${testAppURL}/anchor?id=${testID}`, - actions: ["actions[1337] = 'test1337'"], - }, - ], - cookies: [], - options: [], - }); - - await runner_2.init(); - await runner_2.exec(); - // assignment to an array will return the assigned value, so it's expected for this - // value to be at index 0, and that the value at index 1337 wasn't assigned - // the point of this test is not to ensure that dangerous values can't be returned - // (covered by the test above), only that values at arbitrary indexes can't be assigned - t.assert(runner_2.actionContext.indexOf("test1337") == 0); - t.assert(runner_2.actionContext.indexOf("test1337") != 1337); - await runner_2.finish(); - - const runner_3 = new PlaywrightRunner({ - browser: JobBrowser.CHROMIUM, - steps: [ - { - url: `${testAppURL}/anchor?id=${testID}`, - actions: ["context.test = 'test'"], - }, - ], - cookies: [], - options: [], - }); - - await runner_3.init(); - await runner_3.exec(); - t.false(runner_3.context!.hasOwnProperty("test")); - await runner_3.finish(); -}); - test("PlaywrightRunner steps can interact with buttons", async (t) => { const { testApp, testAppURL } = t.context; const testID = uuid4(); diff --git a/tests/snapshots/swagger.test.ts.md b/tests/snapshots/swagger.test.ts.md index cca26f4..d084ae1 100644 --- a/tests/snapshots/swagger.test.ts.md +++ b/tests/snapshots/swagger.test.ts.md @@ -461,6 +461,64 @@ Generated by [AVA](https://avajs.dev). }, description: 'Default Response', }, + 401: { + content: { + 'application/json': { + schema: { + properties: { + error: { + type: 'string', + }, + message: { + type: 'string', + }, + statusCode: { + enum: [ + 401, + ], + type: 'number', + }, + }, + required: [ + 'statusCode', + 'error', + 'message', + ], + type: 'object', + }, + }, + }, + description: 'Default Response', + }, + 403: { + content: { + 'application/json': { + schema: { + properties: { + error: { + type: 'string', + }, + message: { + type: 'string', + }, + statusCode: { + enum: [ + 403, + ], + type: 'number', + }, + }, + required: [ + 'statusCode', + 'error', + 'message', + ], + type: 'object', + }, + }, + }, + description: 'Default Response', + }, 404: { content: { 'application/json': { @@ -1238,6 +1296,64 @@ Generated by [AVA](https://avajs.dev). }, description: 'Default Response', }, + 401: { + content: { + 'application/json': { + schema: { + properties: { + error: { + type: 'string', + }, + message: { + type: 'string', + }, + statusCode: { + enum: [ + 401, + ], + type: 'number', + }, + }, + required: [ + 'statusCode', + 'error', + 'message', + ], + type: 'object', + }, + }, + }, + description: 'Default Response', + }, + 403: { + content: { + 'application/json': { + schema: { + properties: { + error: { + type: 'string', + }, + message: { + type: 'string', + }, + statusCode: { + enum: [ + 403, + ], + type: 'number', + }, + }, + required: [ + 'statusCode', + 'error', + 'message', + ], + type: 'object', + }, + }, + }, + description: 'Default Response', + }, 404: { content: { 'application/json': { diff --git a/tests/snapshots/swagger.test.ts.snap b/tests/snapshots/swagger.test.ts.snap index 1d4cf00..0ccb4b6 100644 Binary files a/tests/snapshots/swagger.test.ts.snap and b/tests/snapshots/swagger.test.ts.snap differ diff --git a/tsconfig.production.json b/tsconfig.production.json new file mode 100644 index 0000000..2a0c586 --- /dev/null +++ b/tsconfig.production.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "sourceMap": true, + "inlineSources": true, + "sourceRoot": "/" + } +} diff --git a/yarn.lock b/yarn.lock index 4ca26cd..70c3d41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,17 +93,6 @@ resolved "https://registry.yarnpkg.com/@fastify/type-provider-typebox/-/type-provider-typebox-2.4.0.tgz#82e55bbf0da6d7f19e0521959467d4b7e0fefe4c" integrity sha512-dP8KnpfyBD1FT1UxNWCRqSpKG69xYAK53q6MqUjuwYL7xvogXBbH/tpAMc1kZkKmgAHPT0migy9DaYtnkTbIIQ== -"@immobiliarelabs/fastify-sentry@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@immobiliarelabs/fastify-sentry/-/fastify-sentry-5.0.1.tgz#18d896ab3309c173a2597d475eeef64e8e649dbf" - integrity sha512-N7BAPN+TYAdrfwc7L+729aLx388BpJxrMiL0FerurFcfiOIh4V4yNWpWu4PSwl5B2boNkkc9WTn6A9RS8zIE8w== - dependencies: - "@sentry/node" "7.26.0" - "@sentry/tracing" "7.26.0" - "@sentry/utils" "7.26.0" - cookie "^0.5.0" - fastify-plugin "^4.3.0" - "@ioredis/commands@^1.1.1": version "1.2.0" resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" @@ -191,49 +180,60 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@sentry/core@7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.26.0.tgz#8f9fa439b40560edd09b464292d3084e1f16228f" - integrity sha512-ydi236ZoP/xpvLdf7B8seKjCcGc5Z+q9c14tHCFusplPZgLSXcYpiiLIDWmF7OAXO89sSbb1NaFt9YB0LkYdLQ== +"@sentry-internal/tracing@7.46.0": + version "7.46.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.46.0.tgz#26febabe21a2c2cab45a3de75809d88753ec07eb" + integrity sha512-KYoppa7PPL8Er7bdPoxTNUfIY804JL7hhOEomQHYD22rLynwQ4AaLm3YEY75QWwcGb0B7ZDMV+tSumW7Rxuwuw== dependencies: - "@sentry/types" "7.26.0" - "@sentry/utils" "7.26.0" + "@sentry/core" "7.46.0" + "@sentry/types" "7.46.0" + "@sentry/utils" "7.46.0" tslib "^1.9.3" -"@sentry/node@7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.26.0.tgz#fc9290778722d4c1bf7f78564fbcb5790e9a95a8" - integrity sha512-+yxe1YiQS2dRAhJNEaBwWK8Lm3U9IvAzqXTFiGeyvBfo/gewahAb/aVL+0i4fzEGN2nK3+odajdauaq2/iBA1A== +"@sentry/core@7.46.0": + version "7.46.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.46.0.tgz#f377e556d8679f29bde1cce15b1682b6c689d6b7" + integrity sha512-BnNHGh/ZTztqQedFko7vb2u6yLs/kWesOQNivav32ZbsEpVCjcmG1gOJXh2YmGIvj3jXOC9a4xfIuh+lYFcA6A== dependencies: - "@sentry/core" "7.26.0" - "@sentry/types" "7.26.0" - "@sentry/utils" "7.26.0" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" + "@sentry/types" "7.46.0" + "@sentry/utils" "7.46.0" tslib "^1.9.3" -"@sentry/tracing@7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.26.0.tgz#25105f8aec64a0e7113e09674d300190378b1daa" - integrity sha512-UK8EiXxJrDTWD82Oasj2WP/QuQ+wzPlg74vYmxl1ie/LRs6C6wHkilBZwDV9HnDdqAqSjl0al8oBa075lK+U3Q== +"@sentry/integrations@^7.46.0": + version "7.46.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.46.0.tgz#24c02d61c62bd7093c9c748b622858667025b028" + integrity sha512-Y/KreRcROYJif0nM8+kQAkaCvuwGzpqMwLKkC5CfG1xLLDch+OI7HRU98HevyqXNk6YAzQdvBOYXSe7Ny6Zc0A== dependencies: - "@sentry/core" "7.26.0" - "@sentry/types" "7.26.0" - "@sentry/utils" "7.26.0" + "@sentry/types" "7.46.0" + "@sentry/utils" "7.46.0" + localforage "^1.8.1" tslib "^1.9.3" -"@sentry/types@7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.26.0.tgz#2fe8a38a143797abecbcd53175ebf8bf736e18de" - integrity sha512-U2s0q3ALwWFdHJBgn8nrG9bCTJZ3hAqL/I2Si4Mf0ZWnJ/KTJKbtyrputHr8wMbHvX0NZTJGTxFVUO46J+GBRA== +"@sentry/node@^7.46.0": + version "7.46.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.46.0.tgz#f85ee74926372d19d6b6a23f68f19023d7a528a7" + integrity sha512-+GrgJMCye2WXGarRiU5IJHCK27xg7xbPc2XjGojBKbBoZfqxVAWbXEK4bnBQgRGP1pCmrU/M6ZhVgR3dP580xA== + dependencies: + "@sentry-internal/tracing" "7.46.0" + "@sentry/core" "7.46.0" + "@sentry/types" "7.46.0" + "@sentry/utils" "7.46.0" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/types@7.46.0": + version "7.46.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.46.0.tgz#8573ba8676342c594fcfefff4552123278cfec51" + integrity sha512-2FMEMgt2h6u7AoELhNhu9L54GAh67KKfK2pJ1kEXJHmWxM9FSCkizjLs/t+49xtY7jEXr8qYq8bV967VfDPQ9g== -"@sentry/utils@7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.26.0.tgz#4b501064c5220947f210aa2d59e9b8bf60677502" - integrity sha512-nIC1PRyoMBi4QB7XNCWaPDqaQbPayMwAvUm6W3MC5bHPfVZmmFt+3sLZQKUD/E0NeQnJ3vTyPewPF/LfxLOE5A== +"@sentry/utils@7.46.0": + version "7.46.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.46.0.tgz#7a713724db3d1c8bc0aef6d19a7fe2c76db0bdf2" + integrity sha512-elRezDAF84guMG0OVIIZEWm6wUpgbda4HGks98CFnPsrnMm3N1bdBI9XdlxYLtf+ir5KsGR5YlEIf/a0kRUwAQ== dependencies: - "@sentry/types" "7.26.0" + "@sentry/types" "7.46.0" tslib "^1.9.3" "@sinclair/typebox@^0.25.24": @@ -1025,7 +1025,7 @@ fast-uri@^2.0.0, fast-uri@^2.1.0: resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.1.0.tgz#9279432d6b53675c90116b947ed2bbba582d6fb5" integrity sha512-qKRta6N7BWEFVlyonVY/V+BMLgFqktCUV0QjT259ekAIlbVrMaFnFLxJ4s/JPl4tou56S1BzPufI60bLe29fHA== -fastify-plugin@^4.0.0, fastify-plugin@^4.3.0: +fastify-plugin@^4.0.0: version "4.4.0" resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-4.4.0.tgz#ce9fab1352390199c3d55569fea43779699b58ae" integrity sha512-ovwFQG2qNy3jcCROiWpr94Hs0le+c7N/3t7m9aVwbFhkxcR/esp2xu25dP8e617HpQdmeDv+gFX4zagdUhDByw== @@ -1254,6 +1254,11 @@ ignore@~5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -1469,6 +1474,13 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + light-my-request@^5.6.1: version "5.6.1" resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.6.1.tgz#cff5c75d8cb35a354433d75406fea74a2f8bcdb1" @@ -1490,6 +1502,13 @@ load-json-file@^7.0.0: resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-7.0.1.tgz#a3c9fde6beffb6bedb5acf104fad6bb1604e1b00" integrity sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ== +localforage@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"