Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
40 changes: 40 additions & 0 deletions .github/workflows/chapter-api-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: "🧪 API Tests"

on:
workflow_dispatch:

jobs:
apiTests:
name: "🧪 API Tests"
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8.15.5

- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.11.1
cache: "pnpm"

- name: 📥 Download deps
run: pnpm install --frozen-lockfile

- name: 📀 Generate Prisma Client
run: pnpm run generate

- name: 🧪 Run Webapp API Tests
run: pnpm run test chapter_api_tests/
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/postgres
SESSION_SECRET: "secret"
MAGIC_LINK_SECRET: "secret"
ENCRYPTION_KEY: "secret"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@ To setup and develop locally or contribute to the open source project, follow ou
<a href="https://github.com/triggerdotdev/trigger.dev/graphs/contributors">
<img src="https://contrib.rocks/image?repo=triggerdotdev/trigger.dev" />
</a>

## Test
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { describe, expect, it, beforeAll } from '@jest/globals';

/**
* This test suite covers the DELETE /api/v1/projects/{projectRef}/envvars/{env}/{name} endpoint.
*
* It validates:
* 1. Input Validation
* 2. Response Validation
* 3. Response Headers Validation
* 4. Edge Cases & Limit Testing
* 5. Authorization & Authentication
*/

describe('DELETE /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
let axiosInstance: AxiosInstance;
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
const validToken = process.env.API_AUTH_TOKEN || 'VALID_TOKEN';

// Example valid path parameter values (may need to be adjusted to match your actual environment)
const validProjectRef = 'sampleProject';
const validEnv = 'dev';
const validName = 'TEST_VAR';

// Example invalid path parameter values
const invalidProjectRef = '';
const invalidEnv = 123; // wrong type, expecting a string in most cases
const invalidName = '';

// Set up a new Axios instance before all tests
beforeAll(() => {
axiosInstance = axios.create({
baseURL: baseUrl,
headers: {
'Content-Type': 'application/json',
},
validateStatus: () => true, // Allow handling of non-2xx responses
});
});

/**
* Helper function to perform the DELETE request.
* Adjust the function signature if you want extra parameters.
*/
const deleteEnvVar = async (
projectRef: string | number,
env: string | number,
name: string | number,
token?: string
): Promise<AxiosResponse> => {
const endpoint = `/api/v1/projects/${projectRef}/envvars/${env}/${name}`;
const headers = token
? { Authorization: `Bearer ${token}` }
: undefined;

return axiosInstance.delete(endpoint, {
headers,
});
};

it('should delete environment variable successfully [200]', async () => {
const response = await deleteEnvVar(validProjectRef, validEnv, validName, validToken);

// Expect the status code to be 200 (successful deletion)
expect(response.status).toBe(200);

// Check response headers
expect(response.headers['content-type']).toMatch(/application\/json/);

// Check response body schema (assuming a SucceedResponse structure)
// e.g. { success: boolean, message: string, ... }
expect(response.data).toHaveProperty('success');
expect(typeof response.data.success).toBe('boolean');
expect(response.data).toHaveProperty('message');
expect(typeof response.data.message).toBe('string');
});

it('should return 400 or 422 for invalid path parameters', async () => {
// Attempt with an invalid projectRef (empty string)
const response = await deleteEnvVar(invalidProjectRef, validEnv, validName, validToken);

// The API might return 400 or 422
expect([400, 422]).toContain(response.status);

// Check response headers
expect(response.headers['content-type']).toMatch(/application\/json/);

// Check response body (assuming ErrorResponse structure)
// e.g. { error: string, message: string, ... }
expect(response.data).toHaveProperty('error');
expect(response.data).toHaveProperty('message');
});

it('should return 401 or 403 if token is missing or invalid', async () => {
// Missing token
const response = await deleteEnvVar(validProjectRef, validEnv, validName);

// The API might return 401 or 403
expect([401, 403]).toContain(response.status);

// Check response headers
expect(response.headers['content-type']).toMatch(/application\/json/);

// Check response body (assuming ErrorResponse structure)
expect(response.data).toHaveProperty('error');
expect(response.data).toHaveProperty('message');
});

it('should return 404 if environment variable (resource) does not exist', async () => {
// Attempt to delete with a non-existing name
const nonExistentName = 'NON_EXISTENT_VARIABLE';
const response = await deleteEnvVar(validProjectRef, validEnv, nonExistentName, validToken);

// Expect a 404 if resource isn't found
expect(response.status).toBe(404);

// Check response headers
expect(response.headers['content-type']).toMatch(/application\/json/);

// Check response body (assuming ErrorResponse structure)
expect(response.data).toHaveProperty('error');
expect(response.data).toHaveProperty('message');
});

it('should handle large or boundary case values gracefully', async () => {
// Example: extremely large string for projectRef
const largeProjectRef = 'a'.repeat(1000); // Adjust length to test boundary
const response = await deleteEnvVar(largeProjectRef, validEnv, validName, validToken);

// The API might return 400/422 for invalid/boundary issues
expect([400, 422]).toContain(response.status);

// Check headers
expect(response.headers['content-type']).toMatch(/application\/json/);

// Check body
expect(response.data).toHaveProperty('error');
expect(response.data).toHaveProperty('message');
});

// Additional tests can be added here for 500 server errors, rate limiting, etc.
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import axios from 'axios';
import { AxiosResponse } from 'axios';

/************************************************
* DELETE /api/v1/schedules/{schedule_id}
* Summary: Delete a schedule by its ID.
* Only works on IMPERATIVE schedules.
************************************************/

describe('DELETE /api/v1/schedules/:schedule_id', () => {
// Load environment variables
const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
const authToken = process.env.API_AUTH_TOKEN || '';

// Example schedule IDs. In real tests, you might set up test data or mock responses.
// Replace these with actual values.
const validScheduleId = 'VALID_SCHEDULE_ID';
const nonExistentScheduleId = 'NON_EXISTENT_ID';
const invalidScheduleId = '!!!'; // Example of a malformed ID.

// Helper function to perform the DELETE request
const deleteSchedule = async (
scheduleId: string,
token: string | null,
): Promise<AxiosResponse> => {
const url = `${baseURL}/api/v1/schedules/${scheduleId}`;

// Allow all status codes in the response so we can test them in assertions
return axios.delete(url, {
headers: token
? {
Authorization: `Bearer ${token}`,
}
: {},
validateStatus: () => true, // Prevent axios from throwing on non-2xx
});
};

/************************************************
* 1. Input Validation
************************************************/

it('should return 400 or 422 when called with an invalid schedule ID format', async () => {
const response = await deleteSchedule(invalidScheduleId, authToken);
expect([400, 422]).toContain(response.status);
});

it('should return 400 or 422 when called with an empty schedule ID', async () => {
const response = await deleteSchedule('', authToken);
expect([400, 422]).toContain(response.status);
});

/************************************************
* 2. Response Validation (Successful Deletion)
************************************************/

it('should delete the schedule successfully and return status 200', async () => {
// This test assumes the schedule with validScheduleId exists
// and can be deleted. If it doesn’t exist, you may get a 404.
const response = await deleteSchedule(validScheduleId, authToken);

// Confirm it returned a 2xx or specifically 200
expect(response.status).toBe(200);

// Check response headers
expect(response.headers).toHaveProperty('content-type');
expect(response.headers['content-type']).toMatch(/application\/json/i);

// Optional: Validate expected JSON body shape if the API returns a JSON response
// For example, if the API returns: { message: 'Schedule deleted successfully' }
// expect(response.data).toHaveProperty('message', 'Schedule deleted successfully');
});

it('should return 404 when schedule not found', async () => {
// Attempt to delete a schedule ID that does not exist
const response = await deleteSchedule(nonExistentScheduleId, authToken);

expect(response.status).toBe(404);
// Check response headers
expect(response.headers).toHaveProperty('content-type');
expect(response.headers['content-type']).toMatch(/application\/json/i);
// Optional: check response body
// expect(response.data).toHaveProperty('error');
});

/************************************************
* 3. Response Headers Validation
************************************************/

it('should include application/json Content-Type on error responses', async () => {
// Use an invalid schedule ID to force error (e.g., 400)
const response = await deleteSchedule(invalidScheduleId, authToken);

expect([400, 422]).toContain(response.status);
// Verify that the response is in JSON format.
expect(response.headers).toHaveProperty('content-type');
expect(response.headers['content-type']).toMatch(/application\/json/i);
});

/************************************************
* 4. Edge Case & Limit Testing
************************************************/

it('should handle an unauthorized request (missing token) with 401 or 403', async () => {
// Attempt to delete without providing a token
const response = await deleteSchedule(validScheduleId, null);
expect([401, 403]).toContain(response.status);
});

it('should handle nonexistent schedule IDs correctly (already tested: returns 404)', async () => {
// Already covered above, but you can add extra validations.
// This test verifies a second time the correct code is 404.
const response = await deleteSchedule(nonExistentScheduleId, authToken);
expect(response.status).toBe(404);
});

/************************************************
* 5. Testing Authorization & Authentication
************************************************/

it('should return 401 or 403 if the token is invalid', async () => {
const response = await deleteSchedule(validScheduleId, 'INVALID_TOKEN');
expect([401, 403]).toContain(response.status);
});
});
Loading