@teamkeel/testing-runtime 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # `@teamkeel/testing-runtime`
2
+
3
+ `@teamkeel/testng-runtime` is an internal package used by `@teamkeel/testing`. Do not install this package directly.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@teamkeel/testing-runtime",
3
+ "version": "0.0.1",
4
+ "description": "Internal package used by the generated @teamkeel/testing package",
5
+ "exports": "./src/index.mjs",
6
+ "typings": "src/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "test": "vitest run --reporter verbose",
10
+ "format": "npx prettier --write src/**/*"
11
+ },
12
+ "author": "Keel (www.keel.so)",
13
+ "license": "ASL (Apache 2.0)",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "devDependencies": {
18
+ "@types/lodash.ismatch": "^4.4.7",
19
+ "prettier": "2.7.1"
20
+ },
21
+ "dependencies": {
22
+ "jsonwebtoken": "^9.0.0",
23
+ "kysely": "^0.23.4",
24
+ "lodash.ismatch": "^4.4.0",
25
+ "vitest": "^0.27.2"
26
+ }
27
+ }
@@ -0,0 +1,85 @@
1
+ import jwt from "jsonwebtoken";
2
+
3
+ export class ActionExecutor {
4
+ constructor(props) {
5
+ this._identity = props.identity || null;
6
+ this._authToken = props.authToken || null;
7
+
8
+ // Return a proxy which will return a bound version of the
9
+ // _execute method for any unknown properties. This creates
10
+ // the actions API we want but in a dynamic way without needing
11
+ // codegen. We then generate the right type definitions for
12
+ // this class in the @teamkeel/testing package.
13
+ return new Proxy(this, {
14
+ get(target, prop) {
15
+ const v = Reflect.get(...arguments);
16
+ if (v !== undefined) {
17
+ return v;
18
+ }
19
+ return target._execute.bind(target, prop);
20
+ },
21
+ });
22
+ }
23
+ withIdentity(i) {
24
+ return new ActionExecutor({ identity: i });
25
+ }
26
+ withAuthToken(t) {
27
+ return new ActionExecutor({ authToken: t });
28
+ }
29
+ _execute(method, params) {
30
+ const headers = { "Content-Type": "application/json" };
31
+
32
+ // An Identity instance is provided make a JWT
33
+ if (this._identity !== null) {
34
+ headers["Authorization"] =
35
+ "Bearer " +
36
+ jwt.sign(
37
+ {},
38
+ // Not using a signing algorithm, therefore the private key is undefined
39
+ undefined,
40
+ {
41
+ algorithm: "none",
42
+ expiresIn: 60 * 60 * 24,
43
+ subject: this._identity.id,
44
+ issuer: "keel",
45
+ }
46
+ );
47
+ }
48
+
49
+ // If an auth token is provided that can be sent as-is
50
+ if (this._authToken !== null) {
51
+ headers["Authorization"] = "Bearer " + this._authToken;
52
+ }
53
+
54
+ // Use the HTTP JSON API as that returns more friendly errors than
55
+ // the JSON-RPC API.
56
+ return fetch(process.env.KEEL_TESTING_ACTIONS_API_URL + "/" + method, {
57
+ method: "POST",
58
+ body: JSON.stringify(params),
59
+ headers,
60
+ }).then((r) => {
61
+ if (r.status !== 200) {
62
+ // For non-200 first read the response as text
63
+ return r.text().then((t) => {
64
+ let d;
65
+ try {
66
+ d = JSON.parse(t);
67
+ } catch (e) {
68
+ // If JSON parsing fails then throw an error with the
69
+ // response text as the message
70
+ throw new Error(t);
71
+ }
72
+ // Otherwise throw the parsed JSON error response
73
+ // We override toString as otherwise you get expect errors like:
74
+ // `expected to resolve but rejected with "[object Object]"`
75
+ Object.defineProperty(d, "toString", {
76
+ value: () => t,
77
+ enumerable: false,
78
+ });
79
+ throw d;
80
+ });
81
+ }
82
+ return r.json();
83
+ });
84
+ }
85
+ }
@@ -0,0 +1,87 @@
1
+ import jwt from "jsonwebtoken";
2
+
3
+ export class JobExecutor {
4
+ constructor(props) {
5
+ this._identity = props.identity || null;
6
+ this._authToken = props.authToken || null;
7
+
8
+ // Return a proxy which will return a bound version of the
9
+ // _execute method for any unknown properties. This creates
10
+ // the jobs API we want but in a dynamic way without needing
11
+ // codegen. We then generate the right type definitions for
12
+ // this class in the @teamkeel/testing package.
13
+ return new Proxy(this, {
14
+ get(target, prop) {
15
+ const v = Reflect.get(...arguments);
16
+ if (v !== undefined) {
17
+ return v;
18
+ }
19
+ return target._execute.bind(target, prop);
20
+ },
21
+ });
22
+ }
23
+ withIdentity(i) {
24
+ return new JobExecutor({ identity: i });
25
+ }
26
+ withAuthToken(t) {
27
+ return new JobExecutor({ authToken: t });
28
+ }
29
+ _execute(method, params) {
30
+ const headers = { "Content-Type": "application/json" };
31
+
32
+ // An Identity instance is provided make a JWT
33
+ if (this._identity !== null) {
34
+ headers["Authorization"] =
35
+ "Bearer " +
36
+ jwt.sign(
37
+ {},
38
+ // Not using a signing algorithm, therefore the private key is undefined
39
+ undefined,
40
+ {
41
+ algorithm: "none",
42
+ expiresIn: 60 * 60 * 24,
43
+ subject: this._identity.id,
44
+ issuer: "keel",
45
+ }
46
+ );
47
+ }
48
+
49
+ // If an auth token is provided that can be sent as-is
50
+ if (this._authToken !== null) {
51
+ headers["Authorization"] = "Bearer " + this._authToken;
52
+ }
53
+ return fetch(process.env.KEEL_TESTING_JOBS_URL + "/" + method, {
54
+ method: "POST",
55
+ body: JSON.stringify(params),
56
+ headers,
57
+ }).then((r) => {
58
+ if (r.status !== 200) {
59
+ // For non-200 first read the response as text
60
+ return r.text().then((t) => {
61
+ let d;
62
+ try {
63
+ d = JSON.parse(t);
64
+ } catch (e) {
65
+ if ("DEBUG" in process.env) {
66
+ console.log(e);
67
+ }
68
+ // If JSON parsing fails then throw an error with the
69
+ // response text as the message
70
+ throw new Error(t);
71
+ }
72
+ // Otherwise throw the parsed JSON error response
73
+ // We override toString as otherwise you get expect errors like:
74
+ // `expected to resolve but rejected with "[object Object]"`
75
+ Object.defineProperty(d, "toString", {
76
+ value: () => t,
77
+ enumerable: false,
78
+ });
79
+
80
+ throw d;
81
+ });
82
+ }
83
+
84
+ return true;
85
+ });
86
+ }
87
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ // See https://vitest.dev/guide/extending-matchers.html for docs
2
+ // on typing custom matchers
3
+
4
+ interface ActionError {
5
+ code: string;
6
+ message: string;
7
+ }
8
+
9
+ interface CustomMatchers<R = unknown> {
10
+ toHaveAuthorizationError(): void;
11
+ toHaveError(err: Partial<ActionError>): void;
12
+ }
13
+
14
+ declare global {
15
+ namespace Vi {
16
+ interface Assertion extends CustomMatchers {}
17
+ interface AsymmetricMatchersContaining extends CustomMatchers {}
18
+ }
19
+ }
20
+
21
+ export {};
package/src/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ export { sql } from "kysely";
2
+ export { ActionExecutor } from "./ActionExecutor.mjs";
3
+ export { JobExecutor } from "./JobExecutor.mjs";
4
+ export { toHaveError } from "./toHaveError.mjs";
5
+ export { toHaveAuthorizationError } from "./toHaveAuthorizationError.mjs";
@@ -0,0 +1,37 @@
1
+ import { expect, test } from "vitest";
2
+ import "./index";
3
+
4
+ test("toHaveAuthorizationError", async () => {
5
+ const p = Promise.reject({
6
+ code: "ERR_PERMISSION_DENIED",
7
+ });
8
+ await expect(p).toHaveAuthorizationError();
9
+ });
10
+
11
+ test("not.toHaveAuthorizationError", async () => {
12
+ const p = Promise.resolve({
13
+ id: "foo",
14
+ });
15
+ await expect(p).not.toHaveAuthorizationError();
16
+ });
17
+
18
+ test("toHaveError", async () => {
19
+ const p = Promise.reject({
20
+ code: "ERR_INVALID_INPUT",
21
+ message: "Invalid input",
22
+ });
23
+
24
+ await expect(p).toHaveError({
25
+ code: "ERR_INVALID_INPUT",
26
+ });
27
+ });
28
+
29
+ test("not.toHaveError", async () => {
30
+ const p = Promise.resolve({
31
+ id: "foo",
32
+ });
33
+
34
+ await expect(p).not.toHaveError({
35
+ code: "ERR_INVALID_INPUT",
36
+ });
37
+ });
@@ -0,0 +1,21 @@
1
+ export async function toHaveAuthorizationError(received) {
2
+ const { isNot } = this;
3
+ try {
4
+ const v = await received;
5
+ return {
6
+ pass: false,
7
+ message: () => "expected value to reject",
8
+ actual: v,
9
+ };
10
+ } catch (err) {
11
+ return {
12
+ pass: err.code === "ERR_PERMISSION_DENIED",
13
+ message: () =>
14
+ `expected there to be ${isNot ? "no " : ""}ERR_PERMISSION_DENIED error`,
15
+ actual: err,
16
+ expected: {
17
+ ...err,
18
+ },
19
+ };
20
+ }
21
+ }
@@ -0,0 +1,23 @@
1
+ import isMatch from "lodash.ismatch";
2
+
3
+ export async function toHaveError(received, expected) {
4
+ const { isNot } = this;
5
+ try {
6
+ const v = await received;
7
+
8
+ return {
9
+ pass: false,
10
+ message: () => "expected value to reject",
11
+ actual: JSON.stringify(v),
12
+ expected: JSON.stringify(expected),
13
+ };
14
+ } catch (err) {
15
+ return {
16
+ pass: isMatch(err, expected),
17
+ message: () =>
18
+ `expected ${isNot ? "no " : ""} ${JSON.stringify(err)} error`,
19
+ actual: JSON.stringify(err),
20
+ expected: JSON.stringify(expected),
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ // This file is loaded by Vitest because of the ../vitest.config.mjs file
2
+ // which specifies it as a setupFile. When running tests with `keel test`
3
+ // we tell Vitest to load that config file.
4
+
5
+ import { expect } from "vitest";
6
+ import { toHaveError } from "./toHaveError";
7
+ import { toHaveAuthorizationError } from "./toHaveAuthorizationError";
8
+
9
+ expect.extend({
10
+ toHaveError,
11
+ toHaveAuthorizationError,
12
+ });
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ setupFiles: ["./src/vitest-setup"],
6
+ },
7
+ });