Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions packages/test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# @trigger.dev/test

Testing utilities for trigger.dev tasks.

## Installation

```bash
npm install --save-dev @trigger.dev/test
```

## Usage

### Creating a mock task

```typescript
import { mockTask } from "@trigger.dev/test";

const task = mockTask({
id: "test-task",
output: { success: true },
});

// Use the mock task in your tests
const result = await task.triggerAndWait({}).unwrap();
console.log(result); // { success: true }
```

### Unit testing a task with mocked dependencies

```typescript
import { mockTask, testTaskFunction } from "@trigger.dev/test";

// Create a mock for a dependent task
const dependentTask = mockTask({
id: "dependent-task",
output: { result: "mocked-result" },
});

// Test a task that uses the dependent task
const result = await testTaskFunction(mainTask, { input: "test" }, {}, {
mockDependencies: [dependentTask]
});

// Verify the result
expect(result).toEqual({
mainResult: "Processed: mocked-result",
input: "test"
});
```

### Setting up a test environment with mocked tasks

```typescript
import { setupMockTaskEnvironment, mockTask } from "@trigger.dev/test";

// Set up the test environment
const cleanup = setupMockTaskEnvironment((registry) => {
// Register mock tasks
registry.registerMockTask(mockTask({
id: "task-1",
output: { result: "mocked-result-1" },
}));

registry.registerMockTask(mockTask({
id: "task-2",
output: { result: "mocked-result-2" },
}));
});

// Run your tests...

// Clean up
cleanup();
```

### Verifying task triggers

```typescript
import { mockTask, verifyTaskTriggered, getTaskTriggerCount } from "@trigger.dev/test";

const task = mockTask({
id: "test-task",
output: { success: true },
});

// Trigger the task
await task.trigger({ data: "test" });

// Verify the task was triggered
console.log(verifyTaskTriggered(task)); // true
console.log(verifyTaskTriggered(task, { data: "test" })); // true
console.log(getTaskTriggerCount(task)); // 1
```

### Testing hooks

```typescript
import { mockTask, createMockHooks, verifyHookCalled } from "@trigger.dev/test";

const task = mockTask({
id: "test-task",
output: { success: true },
});

const hooks = createMockHooks(task);

// Call the hooks
await hooks.onStart({ data: "test" }, {} as any);
await hooks.onSuccess({ data: "test" }, { success: true });

// Verify the hooks were called
console.log(verifyHookCalled(task, "onStartCalled")); // true
console.log(verifyHookCalled(task, "onSuccessCalled")); // true
```

## API Reference

### Task Mocking

- `mockTask(options)`: Creates a mock task that can be used for testing.
- `isMockTask(task)`: Checks if a task is a mock task.
- `createMockTaskForUnitTest(options)`: Creates a mock task with a custom run function.
- `mockTaskDependencies(taskToTest, dependencies)`: Mocks dependencies for a specific task.

### Task Testing

- `executeMockTask(task, payload, options)`: Executes a task with mocked dependencies.
- `testTaskFunction(task, payload, params, options)`: Tests a task's run function directly.
- `setupMockTaskEnvironment(setupFn)`: Sets up a test environment with mocked tasks.

### Trigger Verification

- `verifyTaskTriggered(task, expectedPayload)`: Verifies that a task was triggered with the expected payload.
- `getTaskTriggerCount(task)`: Gets the number of times a task was triggered.
- `clearAllMockTriggers()`: Clears all mock triggers.

### Hook Testing

- `createMockHooks(task)`: Creates mock hooks for a task.
- `verifyHookCalled(task, hookType)`: Verifies that a specific hook was called for a task.
83 changes: 83 additions & 0 deletions packages/test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"name": "@trigger.dev/test",
"version": "4.0.0-v4-beta.16",
"description": "Testing utilities for trigger.dev tasks",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/triggerdotdev/trigger.dev",
"directory": "packages/test"
},
"type": "module",
"files": [
"dist"
],
"tshy": {
"selfLink": false,
"main": true,
"module": true,
"project": "./tsconfig.build.json",
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts"
},
"sourceDialects": [
"@triggerdotdev/source"
]
},
"typesVersions": {
"*": {
".": [
"dist/commonjs/index.d.ts"
]
}
},
"scripts": {
"clean": "rimraf dist .tshy .tshy-build .turbo",
"build": "tshy && pnpm run update-version",
"dev": "tshy --watch",
"typecheck": "tsc --noEmit",
"update-version": "tsx ../../scripts/updateVersion.ts",
"check-exports": "attw --pack .",
"test": "vitest"
},
"dependencies": {
"@trigger.dev/core": "workspace:4.0.0-v4-beta.16"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.4",
"@trigger.dev/sdk": "workspace:4.0.0-v4-beta.16",
"rimraf": "^3.0.2",
"tshy": "^3.0.2",
"tsx": "4.17.0",
"vitest": "^1.6.0",
"zod": "3.23.8"
},
"peerDependencies": {
"@trigger.dev/sdk": "^4.0.0-v4-beta.16",
"zod": "^3.0.0"
},
"engines": {
"node": ">=18.20.0"
},
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"@triggerdotdev/source": "./src/index.ts",
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
}
},
"main": "./dist/commonjs/index.js",
"types": "./dist/commonjs/index.d.ts",
"module": "./dist/esm/index.js"
}
31 changes: 31 additions & 0 deletions packages/test/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { mockTask, isMockTask, createMockTaskForUnitTest, mockTaskDependencies } from "./mockTask.js";
import { executeMockTask, testTaskFunction } from "./taskTesting.js";
import { setupMockTaskEnvironment } from "./mockTaskRegistry.js";
import { verifyTaskTriggered, getTaskTriggerCount, clearAllMockTriggers } from "./mockTrigger.js";
import { createMockHooks, verifyHookCalled } from "./mockHooks.js";

export * from "./types.js";
export * from "./mockTask.js";
export * from "./mockTrigger.js";
export * from "./mockHooks.js";
export * from "./taskTesting.js";
export * from "./mockTaskRegistry.js";

export const triggerTest = {
mockTask,
isMockTask,
createMockTaskForUnitTest,
mockTaskDependencies,

executeMockTask,
testTaskFunction,

setupMockTaskEnvironment,

verifyTaskTriggered,
getTaskTriggerCount,
clearAllMockTriggers,

createMockHooks,
verifyHookCalled,
};
113 changes: 113 additions & 0 deletions packages/test/src/mockHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Task, TaskIdentifier, TaskOutput, TaskPayload } from "@trigger.dev/sdk/v3";
import { MockHooksCallInfo, TaskRunContext } from "./types.js";

/**
* A registry of all hook calls
*/
class MockHooksRegistry {
private hookCalls: Record<string, MockHooksCallInfo> = {};

/**
* Record a hook call
*/
recordHookCall<TPayload = any, TOutput = any, TError = any>(
taskId: string,
hookType: keyof MockHooksCallInfo,
payload?: TPayload,
output?: TOutput,
error?: TError
) {
if (!this.hookCalls[taskId]) {
this.hookCalls[taskId] = {
onStartCalled: false,
onSuccessCalled: false,
onFailureCalled: false,
onCompleteCalled: false,
};
}

const hookCallInfo = this.hookCalls[taskId];

hookCallInfo[hookType] = true;

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium test

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.

Copilot Autofix

AI 5 months ago

To fix the issue, we need to ensure that taskId cannot be a value like __proto__, constructor, or prototype that could lead to prototype pollution. This can be achieved by validating taskId before using it as a key in the hookCalls object. If taskId contains any of these reserved values, the method should throw an error or handle the case appropriately.

The best approach is to add a validation step at the beginning of the recordHookCall method to check if taskId is one of the reserved keys. If it is, the method should throw an error or return early without making any changes.


Suggested changeset 1
packages/test/src/mockHooks.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/test/src/mockHooks.ts b/packages/test/src/mockHooks.ts
--- a/packages/test/src/mockHooks.ts
+++ b/packages/test/src/mockHooks.ts
@@ -19,2 +19,7 @@
   ) {
+    // Validate taskId to prevent prototype pollution
+    if (["__proto__", "constructor", "prototype"].includes(taskId)) {
+      throw new Error(`Invalid taskId: "${taskId}" is a reserved key.`);
+    }
+
     if (!this.hookCalls[taskId]) {
EOF
@@ -19,2 +19,7 @@
) {
// Validate taskId to prevent prototype pollution
if (["__proto__", "constructor", "prototype"].includes(taskId)) {
throw new Error(`Invalid taskId: "${taskId}" is a reserved key.`);
}

if (!this.hookCalls[taskId]) {
Copilot is powered by AI and may make mistakes. Always verify output.

if (payload !== undefined) {
hookCallInfo.payload = payload;
}

if (output !== undefined) {
hookCallInfo.output = output;
}

if (error !== undefined) {
hookCallInfo.error = error;
}
}

/**
* Get hook calls for a specific task
*/
getHookCallsForTask(taskId: string): MockHooksCallInfo | undefined {
return this.hookCalls[taskId];
}

/**
* Clear all hook calls
*/
clearHookCalls() {
this.hookCalls = {};
}
}

export const mockHooksRegistry = new MockHooksRegistry();

/**
* Create mock hooks for a task
*/
export function createMockHooks<
TIdentifier extends string,
TInput = void,
TOutput = unknown
>(
task: Task<TIdentifier, TInput, TOutput>
) {
return {
onStart: async (payload: TInput, ctx: TaskRunContext) => {
mockHooksRegistry.recordHookCall(task.id, "onStartCalled", payload);
},
onSuccess: async (payload: TInput, output: TOutput) => {
mockHooksRegistry.recordHookCall(task.id, "onSuccessCalled", payload, output);
},
onFailure: async (payload: TInput, error: Error) => {
mockHooksRegistry.recordHookCall(task.id, "onFailureCalled", payload, undefined, error);
},
onComplete: async (payload: TInput, result: { ok: boolean; data?: TOutput; error?: Error }) => {
mockHooksRegistry.recordHookCall(
task.id,
"onCompleteCalled",
payload,
result.ok ? result.data : undefined,
result.ok ? undefined : result.error
);
},
};
}

/**
* Verify that a specific hook was called for a task
*/
export function verifyHookCalled<
TIdentifier extends string,
TInput = void,
TOutput = unknown
>(
task: Task<TIdentifier, TInput, TOutput>,
hookType: keyof Omit<MockHooksCallInfo, "payload" | "output" | "error">
): boolean {
const hookCalls = mockHooksRegistry.getHookCallsForTask(task.id);

if (!hookCalls) {
return false;
}

return hookCalls[hookType];
}
Loading
Loading