diff --git a/.github/workflows/chapter-api-tests.yml b/.github/workflows/chapter-api-tests.yml
new file mode 100644
index 0000000000..55a74705cc
--- /dev/null
+++ b/.github/workflows/chapter-api-tests.yml
@@ -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"
\ No newline at end of file
diff --git a/README.md b/README.md
index 4e3416c4aa..4a5d4773bc 100644
--- a/README.md
+++ b/README.md
@@ -89,3 +89,5 @@ To setup and develop locally or contribute to the open source project, follow ou
+
+## Test
diff --git a/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts b/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
new file mode 100644
index 0000000000..3a5542ed61
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
@@ -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 => {
+ 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.
+});
diff --git a/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.ts b/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.ts
new file mode 100644
index 0000000000..8104a43abb
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.ts
@@ -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 => {
+ 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);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
new file mode 100644
index 0000000000..4e92c1e01f
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
@@ -0,0 +1,183 @@
+import axios, { AxiosError, AxiosResponse } from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+/**
+ * Jest test suite for GET /api/v1/projects/{projectRef}/envvars/{env}/{name}
+ *
+ * This suite covers:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Authorization & Authentication
+ */
+describe('GET /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
+ const baseURL = process.env.API_BASE_URL;
+ const validToken = process.env.API_AUTH_TOKEN;
+
+ // Example valid values (replace with real valid data if available)
+ const validProjectRef = 'my-project';
+ const validEnv = 'production';
+ const validName = 'EXAMPLE_ENV_VAR';
+
+ // Utility function to generate auth header
+ const getAuthHeader = (token?: string) => {
+ return token
+ ? {
+ Authorization: `Bearer ${token}`,
+ }
+ : {};
+ };
+
+ // Helper function to validate EnvVarValue schema (simplified example)
+ const expectEnvVarValueSchema = (data: any) => {
+ // According to #/components/schemas/EnvVarValue
+ // Adjust checks based on actual schema
+ expect(typeof data).toBe('object');
+ expect(typeof data.name).toBe('string');
+ expect(typeof data.value).toBe('string');
+ };
+
+ // Helper function to validate ErrorResponse schema (simplified example)
+ const expectErrorResponseSchema = (data: any) => {
+ // According to #/components/schemas/ErrorResponse
+ // Adjust checks based on actual schema
+ expect(typeof data).toBe('object');
+ expect(typeof data.message).toBe('string');
+ };
+
+ // Happy path test: valid request, valid auth
+ it('should return 200 and a valid EnvVarValue for valid path parameters and authorization', async () => {
+ const url = `${baseURL}/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${validName}`;
+
+ const response: AxiosResponse = await axios.get(url, {
+ headers: {
+ ...getAuthHeader(validToken),
+ },
+ });
+
+ // Response Validation
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ expectEnvVarValueSchema(response.data);
+ });
+
+ // Authorization test: missing or invalid token
+ it('should return 401 or 403 when authorization token is missing', async () => {
+ const url = `${baseURL}/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${validName}`;
+ try {
+ await axios.get(url); // No auth header
+ // If no error is thrown, force fail
+ fail('Expected an unauthorized or forbidden error, but request succeeded.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError?.response?.status);
+ expect(axiosError?.response?.headers['content-type']).toMatch(/application\/json/i);
+ expectErrorResponseSchema(axiosError?.response?.data);
+ }
+ });
+
+ it('should return 401 or 403 when authorization token is invalid', async () => {
+ const url = `${baseURL}/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${validName}`;
+ try {
+ await axios.get(url, {
+ headers: {
+ ...getAuthHeader('invalid-token'),
+ },
+ });
+ fail('Expected an unauthorized or forbidden error, but request succeeded.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError?.response?.status);
+ expect(axiosError?.response?.headers['content-type']).toMatch(/application\/json/i);
+ expectErrorResponseSchema(axiosError?.response?.data);
+ }
+ });
+
+ // 404 Not Found test
+ it('should return 404 if environment variable does not exist', async () => {
+ const invalidName = 'NON_EXISTENT_ENV_VAR';
+ const url = `${baseURL}/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${invalidName}`;
+ try {
+ await axios.get(url, {
+ headers: {
+ ...getAuthHeader(validToken),
+ },
+ });
+ fail('Expected a 404 error, but request succeeded.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError?.response?.status).toBe(404);
+ expect(axiosError?.response?.headers['content-type']).toMatch(/application\/json/i);
+ expectErrorResponseSchema(axiosError?.response?.data);
+ }
+ });
+
+ // 400 or 422 test: invalid path parameters (e.g., empty projectRef)
+ it('should return 400 or 422 if required path parameters are invalid or empty', async () => {
+ const invalidProjectRef = '';
+ const url = `${baseURL}/api/v1/projects/${invalidProjectRef}/envvars/${validEnv}/${validName}`;
+ try {
+ await axios.get(url, {
+ headers: getAuthHeader(validToken),
+ });
+ fail('Expected a 400 or 422 error, but request succeeded.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([400, 422]).toContain(axiosError?.response?.status);
+ expect(axiosError?.response?.headers['content-type']).toMatch(/application\/json/i);
+ expectErrorResponseSchema(axiosError?.response?.data);
+ }
+ });
+
+ // Edge Case: Very long env var name
+ it('should handle a very long name parameter (max length or beyond)', async () => {
+ const longName = 'ENV_VAR_' + 'X'.repeat(1000); // Example large name
+ const url = `${baseURL}/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${longName}`;
+ try {
+ await axios.get(url, {
+ headers: getAuthHeader(validToken),
+ });
+ // If the endpoint allows large names and returns a valid response,
+ // you may adjust your expectations accordingly.
+ // For demonstration, assume it might fail with 400/404.
+ fail('Expected a 400/404 error for an extremely long name, but request succeeded.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // Could be 400 (invalid), 404 (not found), or some other error
+ expect([400, 404, 422]).toContain(axiosError?.response?.status);
+ expect(axiosError?.response?.headers['content-type']).toMatch(/application\/json/i);
+ expectErrorResponseSchema(axiosError?.response?.data);
+ }
+ });
+
+ // Additional test for unexpected server error (simulate if possible)
+ // This is typically not straightforward to trigger programmatically.
+ // For real testing, you might mock the server or use a test double.
+ // We'll just outline the test structure.
+ it('should handle server errors gracefully (5xx)', async () => {
+ // This test is more illustrative; you might force a 5xx by hitting an invalid route
+ // or using a mocking tool, rather than an actual environment.
+ const url = `${baseURL}/api/v1/projects/${validProjectRef}/envvars/${validEnv}/trigger-500-error`;
+ try {
+ await axios.get(url, {
+ headers: getAuthHeader(validToken),
+ });
+ fail('Expected a 5xx error, but request succeeded.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ if (axiosError.response) {
+ expect(axiosError.response.status).toBeGreaterThanOrEqual(500);
+ expect(axiosError.response.status).toBeLessThan(600);
+ expect(axiosError.response.headers['content-type']).toMatch(/application\/json/i);
+ } else {
+ // If no response is returned, it might be a network error or something else.
+ // We can still fail the test or handle accordingly.
+ fail('Did not receive a valid server response for 5xx test.');
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.ts
new file mode 100644
index 0000000000..5e8d5d14de
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.ts
@@ -0,0 +1,155 @@
+import axios, { AxiosError } from 'axios';
+import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
+
+const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
+const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN || 'fake-token';
+
+describe('GET /api/v1/projects/{projectRef}/envvars/{env}', () => {
+ const projectRef = 'testProject';
+ const env = 'development';
+
+ // Create an Axios instance with default headers
+ const axiosInstance = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ Authorization: `Bearer ${API_AUTH_TOKEN}`,
+ },
+ // Allow Jest tests to handle response codes manually
+ validateStatus: () => true,
+ });
+
+ describe('Input validation', () => {
+ it('should return 400 or 422 when projectRef is invalid (e.g. empty string)', async () => {
+ const invalidProjectRef = '';
+ const url = `/api/v1/projects/${invalidProjectRef}/envvars/${env}`;
+
+ const response = await axiosInstance.get(url);
+
+ // Expecting a 400 or 422 for invalid path parameter
+ expect([400, 422]).toContain(response.status);
+ expect(response.data).toBeDefined();
+ });
+
+ it('should return 400 or 422 when env is invalid (e.g. empty string)', async () => {
+ const invalidEnv = '';
+ const url = `/api/v1/projects/${projectRef}/envvars/${invalidEnv}`;
+
+ const response = await axiosInstance.get(url);
+
+ // Expecting a 400 or 422 for invalid path parameter
+ expect([400, 422]).toContain(response.status);
+ expect(response.data).toBeDefined();
+ });
+
+ it('should return 404 when projectRef does not exist', async () => {
+ const nonExistentProjectRef = 'nonExistentProject';
+ const url = `/api/v1/projects/${nonExistentProjectRef}/envvars/${env}`;
+
+ const response = await axiosInstance.get(url);
+
+ // If the project does not exist, we expect a 404
+ expect(response.status).toBe(404);
+ expect(response.data).toBeDefined();
+ });
+ });
+
+ describe('Response validation', () => {
+ it('should return 200 and a valid response schema with existing projectRef/env', async () => {
+ const url = `/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ const response = await axiosInstance.get(url);
+
+ // Check for a successful response
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Basic schema validation (example: expecting an array of envVars)
+ expect(Array.isArray(response.data.envVars)).toBe(true);
+ // Additional checks can be done to verify field types
+ });
+ });
+
+ describe('Response headers validation', () => {
+ it('should include the correct Content-Type header', async () => {
+ const url = `/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ const response = await axiosInstance.get(url);
+
+ // Verify the response Content-Type is JSON
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ it('should handle request with large or no environment variables', async () => {
+ // We cannot easily enforce large or empty responses, but we can check structure
+ const url = `/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ const response = await axiosInstance.get(url);
+
+ // 200 for valid data, possibly 204 if no content
+ expect([200, 204]).toContain(response.status);
+ // If 200, verify envVars is an array
+ if (response.status === 200) {
+ expect(Array.isArray(response.data.envVars)).toBe(true);
+ }
+ });
+
+ it('should handle server error gracefully (simulate if possible)', async () => {
+ // Example of simulating a server error if the API supports such a parameter
+ const url = `/api/v1/projects/${projectRef}/envvars/${env}?simulateServerError=true`;
+
+ const response = await axiosInstance.get(url);
+
+ // If the server is configured to return 500 for simulateServerError
+ if (response.status === 500) {
+ expect(response.data).toBeDefined();
+ } else {
+ // Otherwise, expect normal outcomes for a valid or invalid request
+ expect([200, 400, 404]).toContain(response.status);
+ }
+ });
+ });
+
+ describe('Testing Authorization & Authentication', () => {
+ // Axios instance without authentication
+ const axiosInstanceNoAuth = axios.create({
+ baseURL: API_BASE_URL,
+ validateStatus: () => true,
+ });
+
+ it('should return 200 for valid token', async () => {
+ const url = `/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ const response = await axiosInstance.get(url);
+
+ // Typically 200 if authorized, though 403 may also be possible
+ expect([200, 403]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 for missing token', async () => {
+ const url = `/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ const response = await axiosInstanceNoAuth.get(url);
+
+ // The API might return 401 or 403 in unauthorized cases
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 for invalid token', async () => {
+ const axiosInstanceInvalidAuth = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ Authorization: 'Bearer invalid_token',
+ },
+ validateStatus: () => true,
+ });
+
+ const url = `/api/v1/projects/${projectRef}/envvars/${env}`;
+ const response = await axiosInstanceInvalidAuth.get(url);
+
+ // The API might return 401 or 403 for invalid tokens
+ expect([401, 403]).toContain(response.status);
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.ts
new file mode 100644
index 0000000000..f92799db47
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.ts
@@ -0,0 +1,180 @@
+import axios from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+// Read environment variables
+const baseURL = process.env.API_BASE_URL;
+const authToken = process.env.API_AUTH_TOKEN;
+
+// Helper function to create an Axios instance with or without authorization
+function createAxiosInstance(withAuth = true) {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (withAuth && authToken) {
+ headers['Authorization'] = `Bearer ${authToken}`;
+ }
+
+ return axios.create({
+ baseURL,
+ headers,
+ validateStatus: () => true, // We'll manually check status codes
+ });
+}
+
+describe('GET /api/v1/projects/{projectRef}/runs - List project runs', () => {
+ const validProjectRef = 'my-valid-project'; // Replace with a known valid project reference
+ const invalidProjectRef = ''; // Intentionally invalid (empty) to test error behavior
+
+ // This suite tests the general success scenario with valid inputs.
+ it('should return 200 and a valid JSON body for a valid request', async () => {
+ const axiosInstance = createAxiosInstance(true);
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`);
+
+ // Status code validation
+ expect([200]).toContain(response.status);
+
+ // Headers validation
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Basic structure of the response body according to #/components/schemas/ListRunsResult
+ // For instance, expect it to have items, etc. Adjust based on actual schema
+ expect(response.data).toHaveProperty('items');
+ expect(Array.isArray(response.data.items)).toBe(true);
+ });
+
+ // Test filtering with query params (e.g. status, environment, etc.)
+ it('should return 200 and filter results by status query param when provided', async () => {
+ const axiosInstance = createAxiosInstance(true);
+ // Example of a filter param (adjust key to match #/components/parameters/runsFilterWithEnv)
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`, {
+ params: {
+ status: 'completed',
+ },
+ });
+
+ expect([200]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Check that the response structure is still valid; if runs are returned,
+ // they should match the filter (if your backend enforces it).
+ // If none exist, items should be an empty array.
+ expect(response.data).toHaveProperty('items');
+ expect(Array.isArray(response.data.items)).toBe(true);
+ });
+
+ // Test pagination parameters from #/components/parameters/cursorPagination
+ it('should handle pagination parameters (e.g., cursor) correctly', async () => {
+ const axiosInstance = createAxiosInstance(true);
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`, {
+ params: {
+ // Example: 'cursor' could be a string, depending on your API
+ cursor: 'some-cursor-value',
+ },
+ });
+
+ // The API may return 200 even if the cursor is invalid, or it may return 400.
+ // Adjust the expectation based on how your API handles invalid or missing cursors.
+ expect([200, 400]).toContain(response.status);
+ });
+
+ // Invalid projectRef should result in an error (e.g., 400, 404, or similar)
+ it('should return an error code (400/404) when projectRef is invalid', async () => {
+ const axiosInstance = createAxiosInstance(true);
+ const response = await axiosInstance.get(`/api/v1/projects/${invalidProjectRef}/runs`);
+
+ // Depending on implementation, could be 400, 404, etc.
+ expect([400, 404]).toContain(response.status);
+ });
+
+ // Check invalid query param type
+ it('should return 400 or 422 for invalid query parameter type', async () => {
+ const axiosInstance = createAxiosInstance(true);
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`, {
+ params: {
+ status: 12345, // Passing a number where a string is expected
+ },
+ });
+
+ // The API might return 400 or 422
+ expect([400, 422]).toContain(response.status);
+ });
+
+ // Test an unauthorized request (no token)
+ it('should return 401 or 403 when no auth token is provided', async () => {
+ const axiosInstance = createAxiosInstance(false);
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`);
+
+ // The API might return 401 or 403 if no credentials are provided
+ expect([401, 403]).toContain(response.status);
+ });
+
+ // Test an invalid/expired token
+ it('should return 401 or 403 when an invalid auth token is provided', async () => {
+ // Force an invalid token by passing nonsense
+ const axiosInstance = axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer invalid_token'
+ },
+ validateStatus: () => true,
+ });
+
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`);
+ expect([401, 403]).toContain(response.status);
+ });
+
+ // Test scenario: no runs found (e.g., using a filter that yields zero results)
+ it('should return 200 with an empty list if no runs are found', async () => {
+ const axiosInstance = createAxiosInstance(true);
+ // Filter that is likely to yield no results, adjust as suitable for your API
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`, {
+ params: {
+ status: 'this-status-does-not-exist',
+ },
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toHaveProperty('items');
+ expect(Array.isArray(response.data.items)).toBe(true);
+ expect(response.data.items.length).toBe(0);
+ });
+
+ // Test extremely large or boundary values for pagination or filters
+ it('should handle large pagination values gracefully', async () => {
+ const axiosInstance = createAxiosInstance(true);
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`, {
+ params: {
+ // e.g., possibly an extremely large cursor or some parameter
+ cursor: '999999999999999999999999',
+ },
+ });
+
+ // The API may return 200 with an empty result, 400, or some other code.
+ expect([200, 400]).toContain(response.status);
+ });
+
+ // Optionally test for unexpected server error
+ // Hard to force a 500 unless we tamper with the service or it's a known scenario.
+ // In real tests, you might mock or simulate a server error.
+ it('should handle unexpected internal server errors gracefully (if triggered)', async () => {
+ // This test is not always feasible unless you can cause 500 from the API.
+ // We'll just show the pattern.
+
+ const axiosInstance = createAxiosInstance(true);
+ // Possibly pass in something that you know triggers a 500 if your API has such a scenario.
+ const response = await axiosInstance.get(`/api/v1/projects/${validProjectRef}/runs`, {
+ params: {
+ // Example: dividing by zero if your service does that, or invalid data that triggers 500
+ forceInternalServerError: true,
+ },
+ });
+
+ // The service may return 500 in that scenario.
+ // For demonstration, we accept 500 or 400 or 200, depending on real behavior.
+ expect([200, 400, 500]).toContain(response.status);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.ts
new file mode 100644
index 0000000000..09f68cb8dd
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.ts
@@ -0,0 +1,229 @@
+import axios, { AxiosError } from 'axios';
+import { Response } from 'express'; // Only if needed in your environment
+
+const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
+const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN || 'dummy-auth-token';
+
+/**
+ * Comprehensive Jest test suite for GET /api/v1/runs.
+ *
+ * This test suite covers:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Cases & Limit Testing
+ * 5. Authorization & Authentication
+ */
+describe('GET /api/v1/runs', () => {
+ let axiosInstance = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ Authorization: `Bearer ${API_AUTH_TOKEN}`,
+ },
+ });
+
+ // --------------------------------------------------------------------------
+ // Helper function to validate status code in a list (e.g., [400, 422]).
+ // --------------------------------------------------------------------------
+ function expectStatusInList(status: number, validStatuses: number[]): void {
+ expect(validStatuses).toContain(status);
+ }
+
+ // --------------------------------------------------------------------------
+ // Successful Cases
+ // --------------------------------------------------------------------------
+ describe('Successful Cases', () => {
+ it('should return 200 and a list of runs without filters', async () => {
+ const response = await axiosInstance.get('/api/v1/runs');
+ expect(response.status).toBe(200);
+ // Basic response structure validation.
+ expect(response.data).toBeDefined();
+ // Example check for the shape of the response.
+ // Expecting a structure based on #/components/schemas/ListRunsResult
+ // If you have a validation library, use it here e.g., Joi or ajv.
+
+ // Example:
+ // expect(response.data).toHaveProperty('runs');
+ // expect(Array.isArray(response.data.runs)).toBe(true);
+ });
+
+ it('should return 200 and filtered runs when valid query parameters are provided', async () => {
+ // Example of valid filter parameters.
+ // "runsFilter" might include things like status, createdAt, taskIdentifier, version, etc.
+ const queryParams = {
+ status: 'completed',
+ taskIdentifier: 'task-123',
+ };
+
+ const response = await axiosInstance.get('/api/v1/runs', {
+ params: queryParams,
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.data).toBeDefined();
+ // Additional checks on response schema.
+ });
+
+ it('should return an empty list when no runs match the provided filters', async () => {
+ const queryParams = {
+ status: 'non-existent-status',
+ };
+
+ const response = await axiosInstance.get('/api/v1/runs', {
+ params: queryParams,
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.data).toBeDefined();
+ // Verify empty results array or structure as expected.
+ // Example:
+ // expect(response.data.runs).toBeDefined();
+ // expect(response.data.runs.length).toBe(0);
+ });
+ });
+
+ // --------------------------------------------------------------------------
+ // Input Validation - Invalid Cases
+ // --------------------------------------------------------------------------
+ describe('Input Validation - Invalid Cases', () => {
+ it('should return 400 or 422 when passing invalid data type to filter param', async () => {
+ // Example: passing a non-numeric string to a numeric field.
+ // If the API expects "createdAt" to be a date, pass an invalid string.
+ try {
+ await axiosInstance.get('/api/v1/runs', {
+ params: { createdAt: 'not-a-date' },
+ });
+ // If it does NOT throw, fail the test.
+ fail('Expected an error for invalid filter param, but request succeeded.');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ if (err.response) {
+ expectStatusInList(err.response.status, [400, 422]);
+ }
+ }
+ });
+
+ it('should return 400 or 422 when parameters are out of valid bounds', async () => {
+ // Example: if pageSize or limit must be within a certain range.
+ try {
+ await axiosInstance.get('/api/v1/runs', {
+ params: { limit: 9999999999 },
+ });
+ fail('Expected an error for out-of-bounds parameter, but request succeeded.');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ if (err.response) {
+ expectStatusInList(err.response.status, [400, 422]);
+ }
+ }
+ });
+ });
+
+ // --------------------------------------------------------------------------
+ // Authorization & Authentication
+ // --------------------------------------------------------------------------
+ describe('Authorization & Authentication', () => {
+ it('should return 401 or 403 if no auth token is provided', async () => {
+ const unauthorizedAxios = axios.create({
+ baseURL: API_BASE_URL,
+ });
+
+ try {
+ await unauthorizedAxios.get('/api/v1/runs');
+ fail('Expected an unauthorized error, but request succeeded without token.');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ if (err.response) {
+ expect([401, 403]).toContain(err.response.status);
+ }
+ }
+ });
+
+ it('should return 401 or 403 if auth token is invalid', async () => {
+ const invalidAxios = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ Authorization: 'Bearer invalid-token',
+ },
+ });
+
+ try {
+ await invalidAxios.get('/api/v1/runs');
+ fail('Expected an unauthorized or forbidden error, but request succeeded with invalid token.');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ if (err.response) {
+ expect([401, 403]).toContain(err.response.status);
+ }
+ }
+ });
+ });
+
+ // --------------------------------------------------------------------------
+ // Response Headers Validation
+ // --------------------------------------------------------------------------
+ describe('Response Headers Validation', () => {
+ it('should return Content-Type as application/json on success', async () => {
+ const response = await axiosInstance.get('/api/v1/runs');
+ expect(response.headers['content-type']).toMatch(/application\/(json|octet-stream)/);
+ // Depending on your API, it could be strictly application/json.
+ // Adjust the regex as needed.
+ });
+ });
+
+ // --------------------------------------------------------------------------
+ // Edge Cases & Error Handling
+ // --------------------------------------------------------------------------
+ describe('Edge Cases & Error Handling', () => {
+ it('should handle server errors gracefully (simulating 500)', async () => {
+ // This test might require a mock or a special condition on the API side.
+ // Hypothetical scenario: we trigger an internal server error by passing a specific query param.
+ try {
+ await axiosInstance.get('/api/v1/runs', {
+ params: { triggerServerError: true },
+ });
+ fail('Expected a 500 error or similar, but request succeeded.');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ if (err.response) {
+ // Some APIs return 500, others might return 400 or a custom code.
+ expect([500, 501, 502, 503]).toContain(err.response.status);
+ }
+ }
+ });
+
+ it('should handle very large or negative pagination parameters', async () => {
+ // Large page or negative page.
+ try {
+ await axiosInstance.get('/api/v1/runs', {
+ params: { page: -1 },
+ });
+ fail('Expected an error for negative page parameter, but request succeeded.');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ if (err.response) {
+ expectStatusInList(err.response.status, [400, 422]);
+ }
+ }
+
+ try {
+ await axiosInstance.get('/api/v1/runs', {
+ params: { page: 9999999999 },
+ });
+ fail('Expected an error for extremely large page parameter, but request succeeded.');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ if (err.response) {
+ expectStatusInList(err.response.status, [400, 422]);
+ }
+ }
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.ts
new file mode 100644
index 0000000000..5e19af209a
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.ts
@@ -0,0 +1,184 @@
+import axios, { AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
+
+// Load environment variables
+// Make sure to have API_BASE_URL and API_AUTH_TOKEN set in your environment
+// e.g., export API_BASE_URL="https://your-api.com" && export API_AUTH_TOKEN="secret"
+
+const BASE_URL = process.env.API_BASE_URL as string;
+const AUTH_TOKEN = process.env.API_AUTH_TOKEN as string;
+
+// Create a reusable Axios instance
+const axiosInstance = axios.create({
+ baseURL: BASE_URL,
+ headers: {
+ // Example: using Bearer token for Auth
+ Authorization: `Bearer ${AUTH_TOKEN}`,
+ },
+});
+
+// Utility to check if a given status code is in an allowed array.
+function expectStatusToBeIn(
+ received: number,
+ allowed: number[]
+): void {
+ expect(allowed).toContain(received);
+}
+
+describe('GET /api/v1/schedules/{schedule_id}', () => {
+ // You can define a valid schedule_id (if known) that exists in the system
+ const VALID_SCHEDULE_ID = 'sched_1234';
+
+ beforeAll(async () => {
+ // Optional: Any setup needed before tests, e.g., seeding DB, etc.
+ });
+
+ afterAll(async () => {
+ // Optional: Any cleanup needed after tests
+ });
+
+ it('should retrieve a schedule when provided with a valid schedule_id', async () => {
+ // This test expects a successful 200 response.
+ // Adjust VALID_SCHEDULE_ID if you have a known existing record.
+ let response: AxiosResponse;
+
+ try {
+ response = await axiosInstance.get(`/api/v1/schedules/${VALID_SCHEDULE_ID}`);
+ // Check the 200 OK status
+ expect(response.status).toBe(200);
+ } catch (error) {
+ // Fail the test if any unexpected error occurs
+ throw new Error(`Request failed unexpectedly: ${error}`);
+ }
+
+ // Response Headers Validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Response Body Validation (basic checks)
+ const data = response.data;
+ // Example: Basic schema checks; in a real test, you might use a JSON schema validator.
+ // Here, we assume the returned object has an 'id' property.
+ expect(data).toHaveProperty('id');
+ expect(typeof data.id).toBe('string');
+ // (Add additional schema validations as needed)
+ });
+
+ it('should return 401 or 403 if the request is unauthorized or forbidden', async () => {
+ // Create a new axios instance without the Authorization header
+ const unauthorizedAxios = axios.create({
+ baseURL: BASE_URL,
+ });
+
+ try {
+ await unauthorizedAxios.get(`/api/v1/schedules/${VALID_SCHEDULE_ID}`);
+ // If no error is thrown, the test should fail because we expect a 401 or 403.
+ throw new Error('Request did not fail as expected for unauthorized call.');
+ } catch (error: any) {
+ if (error.response) {
+ const allowedAuthErrorCodes = [401, 403];
+ expectStatusToBeIn(error.response.status, allowedAuthErrorCodes);
+ } else {
+ throw new Error(`Unexpected error: ${error}`);
+ }
+ }
+ });
+
+ it('should return 404 when the schedule_id does not exist', async () => {
+ const NON_EXISTENT_ID = 'sched_non_existent';
+
+ try {
+ await axiosInstance.get(`/api/v1/schedules/${NON_EXISTENT_ID}`);
+ // If no error is thrown, the test should fail because we expect 404.
+ throw new Error('Request did not fail as expected for non-existent resource.');
+ } catch (error: any) {
+ if (error.response) {
+ // A 404 status is expected here.
+ expect(error.response.status).toBe(404);
+ } else {
+ throw new Error(`Unexpected error: ${error}`);
+ }
+ }
+ });
+
+ it('should return 400 or 422 for an invalid schedule_id format', async () => {
+ // Use a clearly invalid format for schedule_id (e.g., containing spaces or special characters)
+ const INVALID_SCHEDULE_ID = '!!!@#';
+
+ try {
+ await axiosInstance.get(`/api/v1/schedules/${INVALID_SCHEDULE_ID}`);
+ throw new Error('Request did not fail as expected for invalid schedule_id.');
+ } catch (error: any) {
+ if (error.response) {
+ const allowedErrorCodes = [400, 422];
+ expectStatusToBeIn(error.response.status, allowedErrorCodes);
+ } else {
+ throw new Error(`Unexpected error: ${error}`);
+ }
+ }
+ });
+
+ it('should handle edge case with an empty schedule_id', async () => {
+ // Some APIs might interpret "/api/v1/schedules/" as a 404 or 400.
+ // The behavior is API-specific, so we handle whichever code is documented.
+ const EMPTY_ID = '';
+
+ try {
+ await axiosInstance.get(`/api/v1/schedules/${EMPTY_ID}`);
+ throw new Error('Request did not fail as expected for empty schedule_id.');
+ } catch (error: any) {
+ if (error.response) {
+ // Depending on API implementation, this could be 400, 404, 422, etc.
+ const possibleErrorCodes = [400, 404, 422];
+ expectStatusToBeIn(error.response.status, possibleErrorCodes);
+ } else {
+ throw new Error(`Unexpected error: ${error}`);
+ }
+ }
+ });
+
+ it('should validate response headers for a valid request', async () => {
+ let response: AxiosResponse;
+ try {
+ response = await axiosInstance.get(`/api/v1/schedules/${VALID_SCHEDULE_ID}`);
+ expect(response.status).toBe(200);
+ } catch (error) {
+ throw new Error(`Request failed unexpectedly: ${error}`);
+ }
+
+ // Check content-type
+ expect(response.headers['content-type'].toLowerCase()).toMatch(/application\/json/);
+
+ // If there are other custom headers the API should return, test them here.
+ // Example:
+ // expect(response.headers).toHaveProperty('x-ratelimit');
+ });
+
+ it('should handle potential 500 server error gracefully', async () => {
+ // Force a scenario that might cause a server error if the API is known to fail for certain inputs.
+ // This test is somewhat contrived; real usage depends on how your API triggers 500 errors.
+ const SERVER_ERROR_ID = 'cause_server_error';
+
+ try {
+ await axiosInstance.get(`/api/v1/schedules/${SERVER_ERROR_ID}`);
+ // If no error, the test passes only if the API truly never returns 500 for such a case.
+ // Typically, you'd want to confirm the expected status code. If this is entirely hypothetical,
+ // you can remove or adapt.
+ // We place a fail if we expect a 500.
+ // throw new Error('Request did not fail as expected for server error scenario.');
+ } catch (error: any) {
+ if (error.response) {
+ // We might check if the status is 500 or not.
+ if (error.response.status === 500) {
+ // Confirm we got a 500.
+ expect(error.response.status).toBe(500);
+ } else {
+ // If we get a different error, handle or fail.
+ // Adjust based on actual API behavior.
+ // e.g., expectStatusToBeIn(error.response.status, [400, 404, 422, 501, etc.]);
+ }
+ } else {
+ throw new Error(`Unexpected error: ${error}`);
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.ts
new file mode 100644
index 0000000000..86892310f7
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.ts
@@ -0,0 +1,216 @@
+import axios, { AxiosResponse } from "axios";
+import { describe, it, expect } from "@jest/globals";
+
+/**
+ * Comprehensive test suite for GET /api/v1/schedules
+ *
+ * This suite tests:
+ * 1) Input Validation
+ * 2) Response Validation
+ * 3) Response Headers Validation
+ * 4) Edge Case & Limit Testing
+ * 5) Authorization & Authentication
+ *
+ * Environment Variables:
+ * - API_BASE_URL: Base URL for API (e.g., https://example.com)
+ * - API_AUTH_TOKEN: Bearer token for authentication (if required)
+ */
+
+describe("GET /api/v1/schedules", () => {
+ const baseUrl = process.env.API_BASE_URL;
+ const endpoint = "/api/v1/schedules";
+ const validAuthToken = process.env.API_AUTH_TOKEN || "";
+
+ /**
+ * Helper function to perform GET request
+ */
+ const getSchedules = async (
+ params?: Record,
+ token: string = validAuthToken
+ ): Promise => {
+ const url = `${baseUrl}${endpoint}`;
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
+ return axios.get(url, { headers, params });
+ };
+
+ /**
+ * 1) Valid request without query params
+ * - Expect 200 response code
+ * - Validate basic response structure
+ */
+ it("should return 200 and valid response schema when called without query params", async () => {
+ const response = await getSchedules();
+ expect(response.status).toBe(200);
+ expect(response.headers["content-type"]).toContain("application/json");
+
+ // Basic schema checks (assuming the response data structure)
+ expect(response.data).toHaveProperty("items");
+ expect(Array.isArray(response.data.items)).toBe(true);
+ // Add any further schema validations if known
+ });
+
+ /**
+ * 1a) Valid request WITH query params (page & perPage as integers)
+ * - Expect 200 response code
+ * - Validate basic response structure & check pagination
+ */
+ it("should accept valid integer query params for page and perPage", async () => {
+ const response = await getSchedules({ page: 1, perPage: 5 });
+ expect(response.status).toBe(200);
+ expect(response.headers["content-type"]).toContain("application/json");
+
+ // Check for pagination fields if the API returns them
+ expect(response.data).toHaveProperty("page");
+ expect(typeof response.data.page).toBe("number");
+ expect(response.data).toHaveProperty("perPage");
+ expect(typeof response.data.perPage).toBe("number");
+ });
+
+ /**
+ * 2) Invalid page parameter type
+ * - Expect 400 or 422
+ */
+ it("should return 400 or 422 for invalid 'page' type (e.g., string)", async () => {
+ try {
+ await getSchedules({ page: "invalid-string" });
+ // If the request does NOT throw an error, fail the test
+ fail("Expected a 400/422 error, but request succeeded.");
+ } catch (error: any) {
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ // In case of network or other errors, fail explicitly
+ fail(`Unexpected error: ${error.message}`);
+ }
+ }
+ });
+
+ /**
+ * 3) Invalid perPage parameter type
+ * - Expect 400 or 422
+ */
+ it("should return 400 or 422 for invalid 'perPage' type (e.g., string)", async () => {
+ try {
+ await getSchedules({ perPage: "invalid-string" });
+ fail("Expected a 400/422 error, but request succeeded.");
+ } catch (error: any) {
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ fail(`Unexpected error: ${error.message}`);
+ }
+ }
+ });
+
+ /**
+ * 4) Boundary values for page/perPage
+ * - e.g., page = 0, perPage = 0, negative numbers, very large numbers
+ * - The API may respond with 400, 422, or some boundary handling
+ */
+ it("should handle boundary values for 'page' and 'perPage'", async () => {
+ // Example boundary: negative page
+ try {
+ await getSchedules({ page: -1 });
+ fail("Expected a 400/422 error for negative page, but request succeeded.");
+ } catch (error: any) {
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ fail(`Unexpected error: ${error.message}`);
+ }
+ }
+ // Example boundary: extremely large perPage
+ try {
+ const largePerPageRes = await getSchedules({ perPage: 999999 });
+ // If the API allows large values, ensure it still returns 200 or error
+ expect([200, 400, 422]).toContain(largePerPageRes.status);
+ } catch (error: any) {
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ fail(`Unexpected error: ${error.message}`);
+ }
+ }
+ });
+
+ /**
+ * 5) Unauthorized request
+ * - Expect 401 or 403 without valid token
+ */
+ it("should return 401 or 403 if token is invalid or missing", async () => {
+ // Test with no token
+ try {
+ await getSchedules({}, "");
+ fail("Expected 401/403 error, but request succeeded.");
+ } catch (error: any) {
+ if (error.response) {
+ expect([401, 403]).toContain(error.response.status);
+ } else {
+ fail(`Unexpected error: ${error.message}`);
+ }
+ }
+ // Test with malformed token
+ try {
+ await getSchedules({}, "Bearer malformed.token");
+ fail("Expected 401/403 error, but request succeeded.");
+ } catch (error: any) {
+ if (error.response) {
+ expect([401, 403]).toContain(error.response.status);
+ } else {
+ fail(`Unexpected error: ${error.message}`);
+ }
+ }
+ });
+
+ /**
+ * 6) Empty data scenario (e.g., requesting a page that does not exist)
+ * - The API might return an empty array/list
+ * - Expect 200 with empty results array or similar
+ */
+ it("should handle empty data scenario gracefully", async () => {
+ // For example, a page far beyond possible results
+ const response = await getSchedules({ page: 9999999 });
+ expect(response.status).toBe(200);
+ expect(Array.isArray(response.data.items)).toBe(true);
+ // If no results, we expect an empty array
+ if (response.data.items.length !== 0) {
+ // If not empty, the test might still pass if the API returns data,
+ // but we can check for general correctness.
+ // This depends on how your API handles out-of-range pages.
+ expect(response.data.page).toBeGreaterThanOrEqual(1);
+ }
+ });
+
+ /**
+ * 7) Response headers validation for successful requests
+ * - Expect content-type to be application/json
+ */
+ it("should return correct response headers on success", async () => {
+ const response = await getSchedules();
+ expect(response.headers["content-type"]).toContain("application/json");
+ // Add any additional relevant headers checks if needed, e.g., Cache-Control, X-RateLimit
+ });
+
+ /**
+ * 8) Large page/perPage value or other stress tests
+ * - This test can help ensure the API handles large queries
+ */
+ it("should handle large pagination query without server error", async () => {
+ try {
+ const response = await getSchedules({ page: 10000, perPage: 10000 });
+ // We just expect it not to crash; check for 200 or possibly 400/422 if the API restricts it.
+ expect([200, 400, 422]).toContain(response.status);
+ } catch (error: any) {
+ // If an internal server error arises, we can detect it here.
+ if (error.response) {
+ if (error.response.status === 500) {
+ fail("Server error (500) occurred, which suggests an unhandled edge case.");
+ } else {
+ expect([400, 422]).toContain(error.response.status);
+ }
+ } else {
+ fail(`Unexpected error: ${error.message}`);
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.ts
new file mode 100644
index 0000000000..b3cca199fc
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.ts
@@ -0,0 +1,180 @@
+import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/**
+ * Comprehensive test suite for the GET /api/v1/timezones endpoint.
+ *
+ * This test suite verifies:
+ * 1. Input Validation
+ * - Parameter correctness (type, required vs. optional, edge cases)
+ * - Proper error handling (400 / 422)
+ * 2. Response Validation
+ * - Response status code correctness (200 on success)
+ * - Response JSON schema checks (content-type, fields)
+ * - Proper error handling (400, 404, 422, etc.)
+ * 3. Response Headers Validation
+ * - Check Content-Type and other relevant headers
+ * 4. Edge Case & Limit Testing
+ * - Invalid/malformed payloads
+ * - Unauthorized (401) / forbidden (403)
+ * - Possible empty responses
+ * - Server errors (500)
+ * 5. Authorization & Authentication
+ * - Valid vs. invalid tokens
+ * - Correct status codes for unauthorized/forbidden requests
+ */
+
+describe('GET /api/v1/timezones', () => {
+ let client: AxiosInstance;
+ let baseURL: string;
+ let authToken: string;
+
+ beforeAll(() => {
+ baseURL = process.env.API_BASE_URL || '';
+ authToken = process.env.API_AUTH_TOKEN || '';
+
+ client = axios.create({
+ baseURL,
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ validateStatus: () => true, // We'll handle status codes manually in tests
+ });
+ });
+
+ describe('Successful Requests', () => {
+ it('should return 200 and valid response without excludeUtc parameter (default: false)', async () => {
+ const response: AxiosResponse = await client.get('/api/v1/timezones');
+
+ expect([200]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ expect(response.data).toBeDefined();
+
+ // Example basic schema check (adjust based on actual schema):
+ // Assuming response.data has a property "timezones" that is an array.
+ // Modify as needed for your actual response body.
+ expect(Array.isArray(response.data.timezones)).toBe(true);
+ });
+
+ it('should return 200 and valid response with excludeUtc=true', async () => {
+ const response: AxiosResponse = await client.get('/api/v1/timezones', {
+ params: {
+ excludeUtc: true,
+ },
+ });
+
+ expect([200]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ expect(response.data).toBeDefined();
+
+ // Additional checks if UTC is supposed to be excluded:
+ // e.g. verify that "UTC" not in the returned timezones, if that is the expectation.
+ if (Array.isArray(response.data.timezones)) {
+ const hasUTC = response.data.timezones.some((tz: string) => tz.toUpperCase().includes('UTC'));
+ expect(hasUTC).toBe(false);
+ }
+ });
+ });
+
+ describe('Input Validation & Edge Cases', () => {
+ it('should handle invalid excludeUtc (e.g., string) and return 400/422', async () => {
+ const response: AxiosResponse = await client.get('/api/v1/timezones', {
+ params: {
+ excludeUtc: 'notBoolean',
+ },
+ });
+
+ // API might return 400 or 422 for invalid inputs.
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should handle excludeUtc as a number and return 400/422', async () => {
+ const response: AxiosResponse = await client.get('/api/v1/timezones', {
+ params: {
+ excludeUtc: 123,
+ },
+ });
+
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should handle excludeUtc=null and return 400/422', async () => {
+ const response: AxiosResponse = await client.get('/api/v1/timezones', {
+ params: {
+ excludeUtc: null,
+ },
+ });
+
+ expect([400, 422]).toContain(response.status);
+ });
+
+ // Additional edge case: If the API can produce an empty array under certain conditions,
+ // test that scenario here if feasible.
+ // For demonstration, we can just assume we check if an empty array is allowed.
+
+ it('should still return 200 with excludeUtc=false (explicit)', async () => {
+ const response: AxiosResponse = await client.get('/api/v1/timezones', {
+ params: {
+ excludeUtc: false,
+ },
+ });
+
+ expect([200]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ expect(response.data).toBeDefined();
+ });
+ });
+
+ describe('Response Headers Validation', () => {
+ it('should include Content-Type header for a valid request', async () => {
+ const response: AxiosResponse = await client.get('/api/v1/timezones');
+ expect([200]).toContain(response.status);
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+
+ // Test any other relevant headers (e.g. Cache-Control, X-RateLimit, etc.) if applicable.
+ });
+ });
+
+ describe('Authorization & Authentication', () => {
+ it('should return 401 or 403 if the Authorization header is missing', async () => {
+ const unauthenticatedClient = axios.create({
+ baseURL,
+ validateStatus: () => true,
+ });
+
+ const response: AxiosResponse = await unauthenticatedClient.get('/api/v1/timezones');
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 if the token is invalid', async () => {
+ const invalidClient = axios.create({
+ baseURL,
+ headers: {
+ Authorization: 'Bearer invalidToken',
+ },
+ validateStatus: () => true,
+ });
+
+ const response: AxiosResponse = await invalidClient.get('/api/v1/timezones');
+ expect([401, 403]).toContain(response.status);
+ });
+ });
+
+ describe('Server Error Handling', () => {
+ // This test is somewhat speculative, as forcing a 500 may require mocking or special setup.
+ // You can adjust or remove it if your test environment cannot generate a 500.
+ it('should handle unexpected server errors (500) gracefully', async () => {
+ // Hypothetical scenario: an invalid path that forces server error or a test double.
+ // Adjust the endpoint or conditions to trigger a 500, if possible.
+ const response: AxiosResponse = await client.get('/api/v1/timezones/force-error');
+ if (response.status === 404) {
+ // If your service returns 404 instead of 500 for non-existent endpoints
+ // then this test can be considered to pass or you can adjust accordingly.
+ expect(response.status).toEqual(404);
+ } else {
+ expect(response.status).toEqual(500);
+ }
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.ts
new file mode 100644
index 0000000000..a503583b1a
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.ts
@@ -0,0 +1,214 @@
+import axios, { AxiosInstance } from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const BASE_URL = process.env.API_BASE_URL || '';
+const VALID_AUTH_TOKEN = process.env.API_AUTH_TOKEN || '';
+
+/**
+ * In a real-world scenario, you would likely fetch/create a valid runId via a setup script
+ * so that you can reliably test a '200' response. For demonstration purposes, we assume:
+ * - "validRunId" is a placeholder that should exist in your system.
+ * - "nonexistentRunId" is a placeholder that will not exist.
+ * Adjust these values as needed.
+ */
+const validRunId = '123';
+const nonexistentRunId = '999999';
+
+/**
+ * Helper function to create an Axios instance with optional auth.
+ */
+function createAxiosClient(token?: string): AxiosInstance {
+ return axios.create({
+ baseURL: BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ validateStatus: () => true, // allow manual handling of status codes
+ });
+}
+
+describe('GET /api/v3/runs/{runId}', () => {
+ let clientWithAuth: AxiosInstance;
+ let clientWithoutAuth: AxiosInstance;
+
+ beforeAll(() => {
+ clientWithAuth = createAxiosClient(VALID_AUTH_TOKEN);
+ clientWithoutAuth = createAxiosClient();
+ });
+
+ /************************************
+ * 1. Input Validation Tests
+ ************************************/
+
+ describe('Input Validation', () => {
+ it('should return 400 when runId is missing (e.g., empty)', async () => {
+ // Some servers/frameworks might route this differently and return 404.
+ // Adjust your expectation based on your actual API behavior.
+ // We expect 400 if the server validates runId and responds with "Invalid or missing run ID".
+
+ const response = await clientWithAuth.get('/api/v3/runs/', {});
+ expect([400, 404]).toContain(response.status);
+
+ if (response.status === 400) {
+ expect(response.data).toHaveProperty('error');
+ expect(response.data.error).toEqual('Invalid or missing run ID');
+ }
+ });
+
+ it('should return 400 when runId is invalid format (non-numeric or malformed)', async () => {
+ const invalidRunId = 'abc@@';
+
+ const response = await clientWithAuth.get(`/api/v3/runs/${invalidRunId}`, {});
+ expect([400, 404]).toContain(response.status);
+
+ if (response.status === 400) {
+ expect(response.data).toHaveProperty('error');
+ expect(response.data.error).toEqual('Invalid or missing run ID');
+ }
+ });
+
+ it('should handle extremely large or special-character runId gracefully', async () => {
+ const largeRunId = '999999999999999999999';
+ const response = await clientWithAuth.get(`/api/v3/runs/${largeRunId}`);
+
+ // Depending on how your API handles out-of-range or invalid numeric IDs,
+ // you might get 400 (bad request) or 404 (not found) if parsed as a number.
+ expect([400, 404]).toContain(response.status);
+ if (response.status === 400) {
+ expect(response.data).toHaveProperty('error');
+ }
+ });
+ });
+
+ /************************************
+ * 2. Response Validation Tests
+ ************************************/
+
+ describe('Response Validation', () => {
+ it('should return 200 and conform to the schema for a valid runId (if run exists)', async () => {
+ // Only run this test if you have a known validRunId that exists.
+ // otherwise, it will likely return 404.
+ // Adjust the expectation accordingly.
+
+ const response = await clientWithAuth.get(`/api/v3/runs/${validRunId}`);
+ if (response.status === 200) {
+ // Basic checks
+ expect(response.data).toBeDefined();
+ // Example schema checks (implementation depends on your actual schema)
+ // Replace these with the actual fields you expect from RetrieveRunResponse
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('status');
+ expect(response.data).toHaveProperty('attempts');
+ } else {
+ // If the runId does not exist, expect 404
+ expect(response.status).toBe(404);
+ }
+ });
+
+ it('should return 404 if the runId does not exist', async () => {
+ const response = await clientWithAuth.get(`/api/v3/runs/${nonexistentRunId}`);
+ expect(response.status).toBe(404);
+ expect(response.data).toHaveProperty('error');
+ expect(response.data.error).toBe('Run not found');
+ });
+
+ it('should return correct error body on 400 responses', async () => {
+ // Using an obviously invalid runId
+ const invalidRunId = '';
+ const response = await clientWithAuth.get(`/api/v3/runs/${invalidRunId}`);
+ expect([400, 404]).toContain(response.status);
+ if (response.status === 400) {
+ expect(response.data).toHaveProperty('error');
+ expect(response.data.error).toBe('Invalid or missing run ID');
+ }
+ });
+ });
+
+ /************************************
+ * 3. Response Headers Validation
+ ************************************/
+
+ describe('Response Headers Validation', () => {
+ it('should return JSON content-type for valid requests', async () => {
+ const response = await clientWithAuth.get(`/api/v3/runs/${validRunId}`);
+ // We allow 200 or 404 in case the run does not exist.
+ expect([200, 404]).toContain(response.status);
+ // Validate content-type header
+ if (response.headers['content-type']) {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ }
+ });
+
+ it('should return JSON content-type for invalid requests (e.g., bad runId)', async () => {
+ const response = await clientWithAuth.get(`/api/v3/runs/!@#`);
+ expect([400, 404]).toContain(response.status);
+ if (response.headers['content-type']) {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ }
+ });
+ });
+
+ /************************************
+ * 4. Edge Case & Limit Testing
+ ************************************/
+
+ describe('Edge Case & Limit Testing', () => {
+ // For GET requests, we typically don’t send a large payload in the request body,
+ // but we can still test the behavior of large or unusually formatted path params.
+
+ it('should handle random runId that does NOT exist (404)', async () => {
+ const randomRunId = '999999999999';
+ const response = await clientWithAuth.get(`/api/v3/runs/${randomRunId}`);
+ expect([404, 400]).toContain(response.status);
+ // If 404, check error message.
+ if (response.status === 404) {
+ expect(response.data.error).toBe('Run not found');
+ }
+ });
+
+ it('should properly handle server errors (e.g., 500) if triggered', async () => {
+ // For many APIs, you might not be able to trigger a 500 easily.
+ // You could mock your API or use a special test fixture.
+ // This is an example test to show how you might handle it.
+
+ // Here, we artificially attempt a runId that might cause the server to fail.
+ // Adjust to match whatever scenario might cause a 500 in your environment.
+
+ const runIdCausingServerError = 'cause-500';
+ const response = await clientWithAuth.get(`/api/v3/runs/${runIdCausingServerError}`);
+ // We include 500 in the array if the server returns 500 for this scenario.
+ expect([400, 404, 500]).toContain(response.status);
+ // If 500, you might check for an error property.
+ if (response.status === 500) {
+ expect(response.data).toHaveProperty('error');
+ }
+ });
+ });
+
+ /************************************
+ * 5. Testing Authorization & Authentication
+ ************************************/
+
+ describe('Authorization & Authentication', () => {
+ it('should return 401 or 403 when no token is provided', async () => {
+ const response = await clientWithoutAuth.get(`/api/v3/runs/${validRunId}`);
+
+ // Depending on API design, you might get 401 or 403.
+ expect([401, 403]).toContain(response.status);
+ expect(response.data).toHaveProperty('error');
+ // If 401, you might check:
+ // expect(response.data.error).toBe('Invalid or Missing API key');
+ });
+
+ it('should return 401 or 403 when an invalid token is provided', async () => {
+ const clientInvalidAuth = createAxiosClient('invalid_token');
+ const response = await clientInvalidAuth.get(`/api/v3/runs/${validRunId}`);
+
+ expect([401, 403]).toContain(response.status);
+ expect(response.data).toHaveProperty('error');
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.ts
new file mode 100644
index 0000000000..393a8b65c9
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.ts
@@ -0,0 +1 @@
+import axios, { AxiosError } from \"axios\";\n\nconst { API_BASE_URL, API_AUTH_TOKEN } = process.env;\n\ndescribe(\"POST /api/v1/projects/:projectRef/envvars/:env/import\", () => {\n const projectRef = \"testProject\";\n const env = \"development\";\n const endpoint = `/api/v1/projects/${projectRef}/envvars/${env}/import`;\n\n // Helper function to get config with or without auth token\n const getAxiosConfig = (includeAuth = true) => {\n const headers: Record = {\n \"Content-Type\": \"application/json\",\n };\n if (includeAuth && API_AUTH_TOKEN) {\n headers[\"Authorization\"] = `Bearer ${API_AUTH_TOKEN}`;\n }\n return {\n baseURL: API_BASE_URL,\n headers,\n validateStatus: () => true, // We'll handle status codes manually\n };\n };\n\n test(\"should successfully import environment variables with valid input (200)\", async () => {\n const payload = {\n envVars: [\n { key: \"TEST_KEY\", value: \"TEST_VALUE\" },\n { key: \"ANOTHER_KEY\", value: \"ANOTHER_VALUE\" },\n ],\n };\n\n const response = await axios.post(endpoint, payload, getAxiosConfig(true));\n expect(response.status).toBe(200);\n expect(response.headers[\"content-type\"]).toContain(\"application/json\");\n // Validate schema from #/components/schemas/SucceedResponse if known\n expect(response.data).toBeDefined();\n // e.g., expect(response.data).toHaveProperty(\"success\", true);\n });\n\n test(\"should return 400 or 422 for invalid request body\", async () => {\n // Missing required fields, or invalid format\n const invalidPayload = {\n // missing \"envVars\" key\n invalidField: [{ key: \"MISSING_KEY\", value: \"MISSING_VALUE\" }],\n };\n\n const response = await axios.post(endpoint, invalidPayload, getAxiosConfig(true));\n expect([400, 422]).toContain(response.status);\n expect(response.headers[\"content-type\"]).toContain(\"application/json\");\n // Validate error structure if needed\n expect(response.data).toBeDefined();\n });\n\n test(\"should return 400 or 422 if the envVars array is empty\", async () => {\n const invalidPayload = {\n envVars: [],\n };\n\n const response = await axios.post(endpoint, invalidPayload, getAxiosConfig(true));\n expect([400, 422]).toContain(response.status);\n expect(response.headers[\"content-type\"]).toContain(\"application/json\");\n expect(response.data).toBeDefined();\n });\n\n test(\"should return 401 or 403 if the request is unauthorized or forbidden\", async () => {\n const payload = {\n envVars: [{ key: \"TEST_KEY\", value: \"TEST_VALUE\" }],\n };\n\n const response = await axios.post(endpoint, payload, getAxiosConfig(false));\n expect([401, 403]).toContain(response.status);\n expect(response.headers[\"content-type\"]).toContain(\"application/json\");\n expect(response.data).toBeDefined();\n });\n\n test(\"should return 404 if the projectRef or env is not found\", async () => {\n const payload = {\n envVars: [{ key: \"TEST_KEY\", value: \"TEST_VALUE\" }],\n };\n\n const badEndpoint = \`/api/v1/projects/nonExistentProject/envvars/${env}/import\`;\n const response = await axios.post(badEndpoint, payload, getAxiosConfig(true));\n expect(response.status).toBe(404);\n expect(response.headers[\"content-type\"]).toContain(\"application/json\");\n expect(response.data).toBeDefined();\n });\n\n test(\"should accept a large payload (stress test)\", async () => {\n const largeEnvVars = [];\n for (let i = 0; i < 50; i++) {\n largeEnvVars.push({ key: \`KEY_${i}\`, value: \`VALUE_${i}\` });\n }\n const payload = { envVars: largeEnvVars };\n\n const response = await axios.post(endpoint, payload, getAxiosConfig(true));\n // The API might handle successfully or might fail if there's a payload size limit\n // We'll check if it's either 200 or 400/422 if the payload is too large or invalid\n // but typically we expect success\n expect([200, 400, 422]).toContain(response.status);\n expect(response.headers[\"content-type\"]).toContain(\"application/json\");\n expect(response.data).toBeDefined();\n });\n\n test(\"should handle boundary value for environment variable\", async () => {\n // Potential test for extremely long key or value\n const veryLongString = \"A\".repeat(5000); // 5k characters\n const payload = {\n envVars: [{ key: veryLongString, value: veryLongString }],\n };\n\n const response = await axios.post(endpoint, payload, getAxiosConfig(true));\n // Expect it to either succeed or fail with 400/422\n expect([200, 400, 422]).toContain(response.status);\n expect(response.headers[\"content-type\"]).toContain(\"application/json\");\n expect(response.data).toBeDefined();\n });\n\n // We typically cannot force a 500 from the client side easily\n // But if there's a known scenario that triggers a 500, we could test it here.\n});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.ts
new file mode 100644
index 0000000000..8822687e01
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.ts
@@ -0,0 +1,246 @@
+import axios, { AxiosResponse } from 'axios';
+import * as dotenv from 'dotenv';
+import { fail } from '@jest/expect';
+
+dotenv.config();
+
+/**
+ * Jest test suite for POST /api/v1/projects/{projectRef}/envvars/{env}
+ * This suite covers:
+ * 1) Input Validation
+ * 2) Response Validation
+ * 3) Response Headers Validation
+ * 4) Edge Case & Limit Testing
+ * 5) Authorization & Authentication Testing
+ */
+describe('POST /api/v1/projects/{projectRef}/envvars/{env}', () => {
+ const baseURL = process.env.API_BASE_URL;
+ const token = process.env.API_AUTH_TOKEN;
+
+ // Project/Env path parameters used for the tests
+ const validProjectRef = 'my-project';
+ const validEnv = 'development';
+
+ // Construct the endpoint using the environment variables
+ const endpoint = `${baseURL}/api/v1/projects/${validProjectRef}/envvars/${validEnv}`;
+
+ describe('Input Validation', () => {
+ it('should create an environment variable with a valid payload (expect 200)', async () => {
+ const payload = {
+ envKey: 'MY_TEST_VAR',
+ envValue: 'some value'
+ };
+
+ const response: AxiosResponse = await axios.post(endpoint, payload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ // Accept 200 as success
+ expect([200]).toContain(response.status);
+ // Basic response shape check
+ expect(response.data).toHaveProperty('success');
+ // Content-Type check
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should return 400 or 422 when required fields are missing', async () => {
+ const invalidPayload = {};
+
+ try {
+ await axios.post(endpoint, invalidPayload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ fail('Expected request to fail with 400 or 422, but it succeeded.');
+ } catch (error: any) {
+ const response = error.response;
+ expect([400, 422]).toContain(response.status);
+ expect(response.data).toHaveProperty('error');
+ expect(response.headers['content-type']).toContain('application/json');
+ }
+ });
+
+ it('should return 400 or 422 for invalid data types', async () => {
+ const invalidPayload = {
+ envKey: 123,
+ envValue: true
+ };
+
+ try {
+ await axios.post(endpoint, invalidPayload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ fail('Expected request to fail with 400 or 422, but it succeeded.');
+ } catch (error: any) {
+ const response = error.response;
+ expect([400, 422]).toContain(response.status);
+ expect(response.data).toHaveProperty('error');
+ }
+ });
+ });
+
+ describe('Response Validation', () => {
+ it('should match the successful response schema on 200', async () => {
+ const payload = {
+ envKey: 'ANOTHER_VAR',
+ envValue: 'another value'
+ };
+
+ const response: AxiosResponse = await axios.post(endpoint, payload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ // Check status
+ expect([200]).toContain(response.status);
+ // Basic shape check
+ expect(response.data).toHaveProperty('success');
+ expect(typeof response.data.success).toBe('boolean');
+ });
+
+ it('should handle resource not found (404)', async () => {
+ // Force a 404 by using an unknown projectRef
+ const invalidEndpoint = `${baseURL}/api/v1/projects/unknownProject/envvars/${validEnv}`;
+
+ try {
+ await axios.post(invalidEndpoint, { envKey: 'test', envValue: 'test' }, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ fail('Expected request to fail with 404, but it succeeded.');
+ } catch (error: any) {
+ const { response } = error;
+ expect(response.status).toBe(404);
+ expect(response.data).toHaveProperty('error');
+ }
+ });
+ });
+
+ describe('Response Headers Validation', () => {
+ it('should return application/json Content-Type on success', async () => {
+ const payload = {
+ envKey: 'HEADER_VAR',
+ envValue: 'header value'
+ };
+
+ const response: AxiosResponse = await axios.post(endpoint, payload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ it('should handle very large payloads gracefully', async () => {
+ const largeValue = 'a'.repeat(5000);
+ const payload = {
+ envKey: 'LARGE_VAR',
+ envValue: largeValue
+ };
+
+ const response: AxiosResponse = await axios.post(endpoint, payload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ // Depending on your service, it might accept or reject large data.
+ // We'll allow 200 (success) or 400/422 if it rejects large input.
+ expect([200, 400, 422]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 if no authorization token is provided', async () => {
+ const payload = {
+ envKey: 'AUTH_TEST',
+ envValue: 'test'
+ };
+
+ try {
+ await axios.post(endpoint, payload);
+ fail('Expected request to fail with 401 or 403, but it succeeded.');
+ } catch (error: any) {
+ const { response } = error;
+ expect([401, 403]).toContain(response.status);
+ }
+ });
+
+ it('should return 500 or appropriate error on server error simulation (if applicable)', async () => {
+ // Simulate a scenario that might cause a server error.
+ // If there's no direct route for server error, you can mock or skip.
+ const brokenPayload = {
+ envKey: null,
+ envValue: 'test'
+ };
+
+ try {
+ await axios.post(endpoint, brokenPayload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ fail('Expected request to fail with 5xx, but it succeeded.');
+ } catch (error: any) {
+ const { response } = error;
+ // Some APIs may return 400/422 for null fields instead of 5xx.
+ expect([500, 400, 422]).toContain(response.status);
+ }
+ });
+ });
+
+ describe('Testing Authorization & Authentication', () => {
+ it('should succeed with a valid token', async () => {
+ const payload = {
+ envKey: 'VALID_TOKEN_TEST',
+ envValue: 'token test'
+ };
+
+ const response: AxiosResponse = await axios.post(endpoint, payload, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ expect([200]).toContain(response.status);
+ });
+
+ it('should fail with 401 or 403 when token is invalid', async () => {
+ const invalidToken = 'invalid.token.value';
+ const payload = {
+ envKey: 'INVALID_TOKEN_TEST',
+ envValue: 'token test'
+ };
+
+ try {
+ await axios.post(endpoint, payload, {
+ headers: {
+ Authorization: `Bearer ${invalidToken}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ fail('Expected request to fail with 401 or 403, but it succeeded.');
+ } catch (error: any) {
+ const { response } = error;
+ expect([401, 403]).toContain(response.status);
+ }
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.ts
new file mode 100644
index 0000000000..a0e62eed22
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.ts
@@ -0,0 +1,183 @@
+import axios, { AxiosInstance, AxiosError } from 'axios';
+import "jest";
+
+/*************************************************************
+ * Jest test suite for POST /api/v1/runs/{runId}/replay
+ *
+ * This test suite is written in TypeScript, uses Jest as the test
+ * framework, Axios for HTTP requests, and follows Prettier styling.
+ *
+ * Before running:
+ * 1. Ensure you have the environment variables set:
+ * - API_BASE_URL (e.g., https://api.example.com)
+ * - API_AUTH_TOKEN (e.g., someAuthToken)
+ * 2. Ensure that you have a known valid runId if you want to test
+ * the successful replay scenario.
+ *
+ * Usage:
+ * jest --runInBand path/to/this/spec.ts
+ *************************************************************/
+
+describe('POST /api/v1/runs/{runId}/replay', () => {
+ let apiClient: AxiosInstance;
+ const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ const authToken = process.env.API_AUTH_TOKEN || 'invalid-token';
+
+ // Use a valid runId if you have one; otherwise, this is a placeholder.
+ // The test might fail unless you supply a valid run ID.
+ const validRunId = 'replace-with-a-valid-run-id';
+
+ beforeAll(() => {
+ // Create a pre-configured axios instance.
+ apiClient = axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ // If auth is required:
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+ });
+
+ /*************************************************************
+ * Helper function to check common response headers.
+ *************************************************************/
+ function checkCommonResponseHeaders(headers: Record): void {
+ // Content-Type should likely be application/json
+ expect(headers['content-type']).toMatch(/application\/json/i);
+
+ // If there are other relevant headers, test them here.
+ // For example:
+ // expect(headers['cache-control']).toBeDefined();
+ // expect(headers['x-ratelimit-remaining']).toBeDefined();
+ }
+
+ /*************************************************************
+ * Positive Test Cases
+ *************************************************************/
+
+ it('should replay a run successfully with a valid runId', async () => {
+ // NOTE: This test assumes that the runId is valid and exists.
+ // The API should return 200 and a body { id: string }
+ try {
+ const response = await apiClient.post(`/api/v1/runs/${validRunId}/replay`);
+
+ // Check status code.
+ expect(response.status).toBe(200);
+
+ // Check response headers.
+ checkCommonResponseHeaders(response.headers);
+
+ // Check response structure.
+ expect(response.data).toBeDefined();
+ expect(response.data).toHaveProperty('id');
+ expect(typeof response.data.id).toBe('string');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // If this fails due to not having a valid runId, you might see 404 or 400.
+ // Handle or fail the test accordingly.
+ console.error('Error executing success test:', axiosError.message);
+ fail(`Expected 200, got ${(axiosError.response && axiosError.response.status) || 'unknown'}`);
+ }
+ });
+
+ /*************************************************************
+ * Negative Test Cases
+ *************************************************************/
+
+ it('should return 400 (or 422) if runId is invalid', async () => {
+ // Example invalid runId.
+ const invalidRunId = '!!!';
+ try {
+ await apiClient.post(`/api/v1/runs/${invalidRunId}/replay`);
+ fail('Expected an error with status 400 or 422 for invalid runId.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // Expecting 400 or 422 based on the API spec.
+ expect([400, 422]).toContain(axiosError.response?.status);
+
+ // Check JSON structure and error message.
+ if (axiosError.response?.data && typeof axiosError.response.data === 'object') {
+ const responseData = axiosError.response.data as Record;
+ expect(responseData.error).toBeDefined();
+ // Could be "Invalid or missing run ID" or another error message.
+ }
+ }
+ });
+
+ it('should return 404 if the run is not found', async () => {
+ // Example runId that presumably does not exist.
+ const nonExistentRunId = 'non-existent-run-id-123';
+ try {
+ await apiClient.post(`/api/v1/runs/${nonExistentRunId}/replay`);
+ fail('Expected a 404 for a run that does not exist.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError.response?.status).toBe(404);
+
+ // Validate response body if present.
+ if (axiosError.response?.data && typeof axiosError.response.data === 'object') {
+ const responseData = axiosError.response.data as Record;
+ expect(responseData.error).toBeDefined();
+ // Could be "Run not found".
+ }
+ }
+ });
+
+ it('should return 401 or 403 if the request is unauthorized or forbidden', async () => {
+ // Create a client with an invalid/empty token to provoke 401/403.
+ const unauthClient = axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer invalid-or-empty-token',
+ },
+ });
+
+ try {
+ await unauthClient.post(`/api/v1/runs/${validRunId}/replay`);
+ fail('Expected a 401 or 403 for unauthorized requests.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError.response?.status);
+
+ if (axiosError.response?.data && typeof axiosError.response.data === 'object') {
+ const responseData = axiosError.response.data as Record;
+ expect(responseData.error).toBeDefined();
+ // Could be "Invalid or Missing API key" or similar.
+ }
+ }
+ });
+
+ /*************************************************************
+ * Edge Case & Limit Testing
+ *************************************************************/
+
+ it('should handle a very large runId (potentially out-of-bounds)', async () => {
+ // Example of extremely large runId.
+ const largeRunId = '999999999999999999999999';
+ try {
+ await apiClient.post(`/api/v1/runs/${largeRunId}/replay`);
+ fail('Expected an error for an out-of-bounds runId.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // The API may return 400, 404, or another error code.
+ expect([400, 404]).toContain(axiosError.response?.status);
+ }
+ });
+
+ it('should return 400 (or 422) if runId is missing', async () => {
+ // Attempt to call the endpoint without specifying runId in the path.
+ // This is syntactically not valid, so the outcome might be 404 in some frameworks.
+ // We can demonstrate the concept by passing an empty string.
+ const missingRunId = '';
+ try {
+ await apiClient.post(`/api/v1/runs/${missingRunId}/replay`);
+ fail('Expected an error for missing runId.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // Depending on the implementation, it could be 400, 404, or 422.
+ expect([400, 404, 422]).toContain(axiosError.response?.status);
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.ts
new file mode 100644
index 0000000000..490aef3386
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.ts
@@ -0,0 +1,201 @@
+import axios from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+describe('POST /api/v1/runs/{runId}/reschedule', () => {
+ const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
+ const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN || 'test-token';
+
+ const validRunId = '12345';
+ const nonExistentRunId = '99999';
+ const invalidRunId = 'abc';
+ const delayedRunId = 'delayedRun123'; // Suppose this run is actually in DELAYED state
+
+ // Example request body for updating the delay (assuming it expects a field "delayInSeconds")
+ const validPayload = {
+ delayInSeconds: 60,
+ };
+
+ // Utility function for setting headers
+ const getHeaders = (token?: string) => {
+ return {
+ Authorization: token ? `Bearer ${token}` : '',
+ 'Content-Type': 'application/json',
+ };
+ };
+
+ // 1. Successful Reschedule - Valid Input
+ it('should reschedule a delayed run (status 200) with valid runId and payload', async () => {
+ try {
+ const response = await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${delayedRunId}/reschedule`,
+ validPayload,
+ {
+ headers: getHeaders(API_AUTH_TOKEN),
+ }
+ );
+
+ // Response status and header checks
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Basic response body validation (example checks)
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('status');
+ // Additional schema checks for RetrieveRunResponse can be added here.
+ } catch (error) {
+ // If an error is thrown, fail the test
+ throw error;
+ }
+ });
+
+ // 2. Invalid Payload (e.g., negative delay)
+ it('should return 400 or 422 when the payload is invalid', async () => {
+ const invalidPayload = {
+ delayInSeconds: -1, // negative value is invalid
+ };
+
+ try {
+ await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${delayedRunId}/reschedule`,
+ invalidPayload,
+ {
+ headers: getHeaders(API_AUTH_TOKEN),
+ }
+ );
+
+ // We expect an error, so if we get here, the test fails.
+ fail('Expected a 400 or 422 error for invalid payload, but the request succeeded.');
+ } catch (error: any) {
+ // Confirm we got the correct error status
+ const status = error.response?.status;
+ expect([400, 422]).toContain(status);
+ // Check error body
+ expect(error.response?.data).toHaveProperty('error');
+ }
+ });
+
+ // 3. Invalid Path Parameter (non-numeric or malformed runId)
+ it('should return 400 for invalid path parameter (non-numeric runId)', async () => {
+ try {
+ await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${invalidRunId}/reschedule`,
+ validPayload,
+ {
+ headers: getHeaders(API_AUTH_TOKEN),
+ }
+ );
+
+ fail('Expected a 400 error for invalid runId, but the request succeeded.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect(status).toBe(400);
+ expect(error.response?.data).toHaveProperty('error');
+ }
+ });
+
+ // 4. Non-existent Run
+ it('should return 404 if the run does not exist', async () => {
+ try {
+ await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${nonExistentRunId}/reschedule`,
+ validPayload,
+ {
+ headers: getHeaders(API_AUTH_TOKEN),
+ }
+ );
+
+ fail('Expected a 404 error for non-existent run, but the request succeeded.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect(status).toBe(404);
+ expect(error.response?.data).toHaveProperty('error');
+ }
+ });
+
+ // 5. Authorization & Authentication Tests
+ it('should return 401 or 403 if the authorization token is missing or invalid', async () => {
+ // Missing token
+ try {
+ await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${validRunId}/reschedule`,
+ validPayload,
+ {
+ headers: getHeaders(''), // empty token
+ }
+ );
+
+ fail('Expected a 401 or 403 error for missing token, but the request succeeded.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([401, 403]).toContain(status);
+ expect(error.response?.data).toHaveProperty('error');
+ }
+
+ // Invalid token
+ try {
+ await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${validRunId}/reschedule`,
+ validPayload,
+ {
+ headers: getHeaders('invalid-token'),
+ }
+ );
+
+ fail('Expected a 401 or 403 error for invalid token, but the request succeeded.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([401, 403]).toContain(status);
+ expect(error.response?.data).toHaveProperty('error');
+ }
+ });
+
+ // 6. Run Not in DELAYED State
+ it('should handle cases where the run is not in DELAYED state (return 400)', async () => {
+ const notDelayedRunId = 'activeRun123';
+
+ try {
+ await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${notDelayedRunId}/reschedule`,
+ validPayload,
+ {
+ headers: getHeaders(API_AUTH_TOKEN),
+ }
+ );
+
+ fail('Expected a 400 error if run is not in DELAYED state, but request succeeded.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect(status).toBe(400);
+ expect(error.response?.data).toHaveProperty('error');
+ }
+ });
+
+ // 7. Large or Boundary Values for Delay
+ it('should handle large values for delayInSeconds', async () => {
+ const largePayload = {
+ delayInSeconds: 999999999,
+ };
+
+ try {
+ const response = await axios.post(
+ `${API_BASE_URL}/api/v1/runs/${delayedRunId}/reschedule`,
+ largePayload,
+ {
+ headers: getHeaders(API_AUTH_TOKEN),
+ }
+ );
+
+ // The API might allow large values or reject them with 400/422.
+ expect([200, 400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ } catch (error: any) {
+ // If it rejects, it should be 400/422.
+ if (error.response) {
+ const status = error.response.status;
+ expect([400, 422]).toContain(status);
+ } else {
+ throw error; // Other network/unknown errors
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.ts
new file mode 100644
index 0000000000..8a13a5e560
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.ts
@@ -0,0 +1,130 @@
+import axios, { AxiosInstance } from 'axios';
+
+const baseUrl = process.env.API_BASE_URL;
+const token = process.env.API_AUTH_TOKEN;
+
+describe('POST /api/v1/schedules/{schedule_id}/activate', () => {
+ let client: AxiosInstance;
+
+ beforeAll(() => {
+ client = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ Authorization: token ? `Bearer ${token}` : '',
+ 'Content-Type': 'application/json',
+ },
+ });
+ });
+
+ describe('Input Validation', () => {
+ test('should return 400 or 422 when schedule_id is invalid (e.g. empty string)', async () => {
+ const invalidScheduleId = '';
+ try {
+ await client.post(`/api/v1/schedules/${invalidScheduleId}/activate`);
+ fail('Expected an error, but got success response');
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+
+ test('should return 400 or 422 when schedule_id is a bad format (e.g. malformed string)', async () => {
+ const invalidScheduleId = '!!!@@@';
+ try {
+ await client.post(`/api/v1/schedules/${invalidScheduleId}/activate`);
+ fail('Expected an error, but got success response');
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+ });
+
+ describe('Response Validation', () => {
+ test('should activate schedule successfully with a valid schedule_id', async () => {
+ // Replace with a valid imperative schedule ID known to exist
+ const validScheduleId = 'some_existing_imperative_schedule_id';
+
+ const response = await client.post(`/api/v1/schedules/${validScheduleId}/activate`);
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ // Validate response body structure (example checks below)
+ expect(response.data).toHaveProperty('id', validScheduleId);
+ expect(response.data).toHaveProperty('status', 'ACTIVE');
+ // Additional schema validations can be added here
+ });
+ });
+
+ describe('Response Headers Validation', () => {
+ test('should include relevant response headers', async () => {
+ // Replace with a valid imperative schedule ID known to exist
+ const validScheduleId = 'some_existing_imperative_schedule_id';
+
+ const response = await client.post(`/api/v1/schedules/${validScheduleId}/activate`);
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ // If the API includes other headers like cache-control or rate-limiting, add tests here
+ // Example: expect(response.headers).toHaveProperty('cache-control');
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ test('should return 404 if schedule is not found', async () => {
+ const nonExistentScheduleId = 'non-existent-schedule-id';
+ try {
+ await client.post(`/api/v1/schedules/${nonExistentScheduleId}/activate`);
+ fail('Expected 404 Not Found, but received success response');
+ } catch (error: any) {
+ expect(error.response.status).toBe(404);
+ }
+ });
+
+ test('should handle large schedule_id gracefully', async () => {
+ // Use an artificially large schedule ID
+ const largeScheduleId = 'a'.repeat(1000);
+ try {
+ await client.post(`/api/v1/schedules/${largeScheduleId}/activate`);
+ fail('Expected an error, but got success response');
+ } catch (error: any) {
+ // Could be 400, 422, or 404 depending on implementation
+ expect([400, 422, 404]).toContain(error.response.status);
+ }
+ });
+ });
+
+ describe('Testing Authorization & Authentication', () => {
+ test('should return 401 or 403 if token is missing', async () => {
+ const tempClient = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ // Replace with a valid imperative schedule ID
+ const validScheduleId = 'some_existing_imperative_schedule_id';
+
+ try {
+ await tempClient.post(`/api/v1/schedules/${validScheduleId}/activate`);
+ fail('Expected a 401 or 403 error');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error.response.status);
+ }
+ });
+
+ test('should return 401 or 403 if token is invalid', async () => {
+ const tempClient = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ Authorization: 'Bearer invalid_token',
+ 'Content-Type': 'application/json',
+ },
+ });
+ // Replace with a valid imperative schedule ID
+ const validScheduleId = 'some_existing_imperative_schedule_id';
+
+ try {
+ await tempClient.post(`/api/v1/schedules/${validScheduleId}/activate`);
+ fail('Expected a 401 or 403 error');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error.response.status);
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.ts
new file mode 100644
index 0000000000..58f5af8270
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.ts
@@ -0,0 +1,179 @@
+import axios, { AxiosResponse, AxiosError } from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const API_BASE_URL = process.env.API_BASE_URL;
+const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN;
+
+/**
+ * This test suite verifies the behavior of the POST /api/v1/schedules/{schedule_id}/deactivate endpoint.
+ * It covers:
+ * 1. Input validation (required parameters, data types, edge cases)
+ * 2. Response validation (status codes, body structure, error handling)
+ * 3. Response headers validation (Content-Type, etc.)
+ * 4. Edge cases & limit testing (boundary values, unauthorized, not found, etc.)
+ * 5. Authorization & authentication tests (valid, invalid tokens)
+ */
+
+describe('POST /api/v1/schedules/:schedule_id/deactivate', () => {
+ const validScheduleId = 'valid-imperative-schedule-id'; // Replace with a real or mocked schedule ID.
+ const nonExistentScheduleId = 'non-existent-id';
+
+ // Helper function to build the request URL.
+ const buildUrl = (scheduleId: string) => {
+ return `${API_BASE_URL}/api/v1/schedules/${scheduleId}/deactivate`;
+ };
+
+ // Shared config for axios including headers.
+ const axiosConfig = {
+ headers: {
+ Authorization: `Bearer ${API_AUTH_TOKEN}`,
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ /**
+ * Test: Successful deactivation with a valid schedule ID.
+ * Expect: 200 OK, application/json response, valid ScheduleObject.
+ */
+ it('should deactivate a valid imperative schedule successfully', async () => {
+ expect(API_BASE_URL).toBeDefined();
+ expect(API_AUTH_TOKEN).toBeDefined();
+
+ try {
+ const response: AxiosResponse = await axios.post(buildUrl(validScheduleId), {}, axiosConfig);
+
+ // Check response status
+ expect(response.status).toBe(200);
+
+ // Check response headers
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate response body structure (basic checks for ScheduleObject)
+ const data = response.data;
+ // For a more robust check, assert each required property in your schema.
+ expect(data).toHaveProperty('id');
+ expect(typeof data.id).toBe('string');
+ } catch (error) {
+ // If the API is down or the test config is invalid, handle here.
+ const axiosError = error as AxiosError;
+ // You could check axiosError.response to see if it returned a known error code.
+ if (axiosError.response) {
+ // If we expect success but got an error, fail the test.
+ fail(
+ `Expected 200, received ${axiosError.response.status}: ${JSON.stringify(
+ axiosError.response.data
+ )}`
+ );
+ } else {
+ fail(`Request failed: ${axiosError.message}`);
+ }
+ }
+ });
+
+ /**
+ * Test: Invalid schedule_id (empty or malformed) should return 400 or 422.
+ * Expect: 400/422 status, proper error handling.
+ */
+ it('should return 400 or 422 when schedule_id is invalid (e.g., empty string)', async () => {
+ const invalidScheduleId = '';
+ try {
+ await axios.post(buildUrl(invalidScheduleId), {}, axiosConfig);
+ // If the request succeeds here, it means the API did not validate properly.
+ fail('Expected 400 or 422 for invalid schedule_id but got a success response.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([400, 422]).toContain(axiosError.response?.status);
+ }
+ });
+
+ /**
+ * Test: Non-existent schedule ID should return 404.
+ */
+ it('should return 404 for a non-existent schedule_id', async () => {
+ try {
+ await axios.post(buildUrl(nonExistentScheduleId), {}, axiosConfig);
+ fail('Expected 404 for non-existent schedule_id but got success response.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError.response?.status).toBe(404);
+ }
+ });
+
+ /**
+ * Test: Missing or invalid auth token should return 401 or 403.
+ */
+ it('should return 401 or 403 for unauthorized or forbidden requests', async () => {
+ const invalidConfig = {
+ headers: {
+ Authorization: 'Bearer invalid_token',
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ try {
+ await axios.post(buildUrl(validScheduleId), {}, invalidConfig);
+ fail('Expected 401 or 403 for invalid token but got success response.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError.response?.status);
+ }
+ });
+
+ /**
+ * Test: Check response headers for correctness on a valid request.
+ */
+ it('should include correct response headers on success', async () => {
+ try {
+ const response: AxiosResponse = await axios.post(buildUrl(validScheduleId), {}, axiosConfig);
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ // Optionally check for other headers like Cache-Control, X-RateLimit, etc.
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ fail(`Request failed unexpectedly. Status: ${axiosError.response?.status}`);
+ }
+ });
+
+ /**
+ * Test: Large or boundary schedule_id (edge case) - usually returns 404 or maybe 400 if invalid.
+ */
+ it('should handle a very long schedule_id gracefully', async () => {
+ const longScheduleId = 'a'.repeat(1000); // 1000-char string.
+ try {
+ await axios.post(buildUrl(longScheduleId), {}, axiosConfig);
+ fail('Expected 404 or 400/422 for extremely long schedule_id.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // Depending on API design, might be 400/422 or 404
+ expect([400, 404, 422]).toContain(axiosError.response?.status);
+ }
+ });
+
+ /**
+ * Test: Handling a potential server error (simulate if possible or check how the API handles errors).
+ * This may require mocking or an environment scenario where the server triggers a 5xx error.
+ * We'll demonstrate a 500 check if triggered.
+ */
+ it('should handle server errors (5xx) gracefully', async () => {
+ // This test is conceptual: the real trigger for a 500 would be on the server side.
+ // If you can force a 500 from the server by some condition, do so here.
+ // In this example, we'll call a known invalid path to simulate a possible 500 scenario.
+
+ try {
+ await axios.post(`${API_BASE_URL}/api/v1/schedules/trigger-500/deactivate`, {}, axiosConfig);
+ fail('Expected 500 Internal Server Error but request succeeded.');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // If the server truly returns a 500 for that path or under special conditions:
+ if (axiosError.response?.status !== 500) {
+ // Not a 500 error, but at least we caught an error.
+ // Adjust this assertion based on your actual API behavior.
+ fail(`Expected 500, but received ${axiosError.response?.status}`);
+ } else {
+ expect(axiosError.response?.status).toBe(500);
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts
new file mode 100644
index 0000000000..a7f2cef619
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts
@@ -0,0 +1,156 @@
+import axios, { AxiosError } from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+const authToken = process.env.API_AUTH_TOKEN || '';
+
+describe('POST /api/v1/schedules', () => {
+ // Helper function to create an Axios instance
+ const createAxiosInstance = (token?: string) => {
+ return axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ validateStatus: () => true, // Allows us to handle non-2xx status codes in tests
+ });
+ };
+
+ // A valid payload conforming to a hypothetical ScheduleObject schema
+ // Adjust this payload as necessary to match your actual schema requirements
+ const validSchedulePayload = {
+ type: 'IMPERATIVE',
+ name: 'Test Schedule',
+ frequency: 'daily',
+ startTime: '2023-09-10T10:00:00Z',
+ endTime: '2023-09-10T11:00:00Z',
+ };
+
+ it('should create a schedule with valid data (expect 200)', async () => {
+ const axiosInstance = createAxiosInstance(authToken);
+
+ const response = await axiosInstance.post('/api/v1/schedules', validSchedulePayload);
+
+ // Response Validation
+ expect([200]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ // Here you can add more specific checks based on the schema of the response
+ // e.g. if the schema requires an 'id' field, you could do:
+ // expect(response.data).toHaveProperty('id');
+ // expect(response.data).toHaveProperty('type', 'IMPERATIVE');
+ });
+
+ it('should return 400 or 422 for missing required fields', async () => {
+ const axiosInstance = createAxiosInstance(authToken);
+
+ // Missing the 'name' field
+ const invalidPayload = {
+ type: 'IMPERATIVE',
+ frequency: 'daily',
+ startTime: '2023-09-10T10:00:00Z',
+ endTime: '2023-09-10T11:00:00Z',
+ };
+
+ const response = await axiosInstance.post('/api/v1/schedules', invalidPayload);
+
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should return 400 or 422 for invalid parameter types', async () => {
+ const axiosInstance = createAxiosInstance(authToken);
+
+ // Invalid type for 'frequency' (should be string but provided as number)
+ const invalidPayload = {
+ ...validSchedulePayload,
+ frequency: 12345,
+ };
+
+ const response = await axiosInstance.post('/api/v1/schedules', invalidPayload);
+
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should handle large payloads (expect success or appropriate error code)', async () => {
+ const axiosInstance = createAxiosInstance(authToken);
+
+ // Create a large string for testing
+ const largeString = 'a'.repeat(10000); // Adjust size as needed
+
+ const largePayload = {
+ ...validSchedulePayload,
+ description: largeString,
+ };
+
+ const response = await axiosInstance.post('/api/v1/schedules', largePayload);
+
+ // Depending on server logic, it might accept or reject large payloads
+ // If accepted, might return 200; if not, might return 413, 400, or another code
+ // Adjust expectations based on your API's behavior
+ expect([200, 400, 413, 422]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 if no auth token is provided', async () => {
+ const axiosInstance = createAxiosInstance(); // No token passed
+
+ const response = await axiosInstance.post('/api/v1/schedules', validSchedulePayload);
+
+ // Unauthorized or forbidden
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 if an invalid auth token is provided', async () => {
+ const axiosInstance = createAxiosInstance('InvalidToken');
+
+ const response = await axiosInstance.post('/api/v1/schedules', validSchedulePayload);
+
+ // Unauthorized or forbidden
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should handle empty payload gracefully (expect 400 or 422)', async () => {
+ const axiosInstance = createAxiosInstance(authToken);
+
+ const response = await axiosInstance.post('/api/v1/schedules', {});
+
+ // Missing all required fields, expecting 400 or 422
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should validate response headers correctly for a valid request', async () => {
+ const axiosInstance = createAxiosInstance(authToken);
+
+ const response = await axiosInstance.post('/api/v1/schedules', validSchedulePayload);
+
+ // Verify Content-Type is application/json
+ expect(response.headers['content-type']).toContain('application/json');
+ // Check other potential headers if applicable, e.g. Cache-Control, X-RateLimit
+ // expect(response.headers).toHaveProperty('cache-control');
+ // expect(response.headers).toHaveProperty('x-ratelimit-limit');
+ });
+
+ it('should handle server errors (simulated check for 500)', async () => {
+ // Forcing a 500 is highly dependent on the API. If you have a scenario that
+ // triggers an internal server error, you can test it here. In many cases,
+ // you might mock the server response or handle a known failure scenario.
+ // The following lines demonstrate how you would handle an error gracefully.
+ const axiosInstance = createAxiosInstance(authToken);
+
+ try {
+ // Attempting to trigger error by sending unexpected data
+ const response = await axiosInstance.post('/api/v1/schedules', { unexpectedKey: 'foo' });
+ // Not guaranteed to throw or return 500. Adjust based on your API.
+ expect([500, 400, 422]).toContain(response.status);
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // If the request fails and throws, handle it here
+ if (axiosError.response) {
+ expect(axiosError.response.status).toBe(500);
+ } else {
+ throw error;
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.ts
new file mode 100644
index 0000000000..9e030c8036
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.ts
@@ -0,0 +1,175 @@
+import axios, { AxiosInstance } from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+// NOTE: Ensure you have set API_BASE_URL and API_AUTH_TOKEN in your environment
+// For example:
+// export API_BASE_URL=https://your-api.com
+// export API_AUTH_TOKEN=someAuthToken
+
+const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+
+// Helper function to create an axios instance with or without auth token.
+const createClient = (withAuth: boolean = true): AxiosInstance => {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (withAuth && process.env.API_AUTH_TOKEN) {
+ headers.Authorization = `Bearer ${process.env.API_AUTH_TOKEN}`;
+ }
+
+ return axios.create({
+ baseURL,
+ headers,
+ validateStatus: () => true, // Allow us to handle all status codes in tests
+ });
+};
+
+describe('POST /api/v1/tasks/batch - Batch trigger tasks', () => {
+ const endpoint = '/api/v1/tasks/batch';
+
+ it('should trigger tasks successfully with a valid payload (200)', async () => {
+ const client = createClient();
+
+ // A minimal valid payload, adjust fields to match your actual schema.
+ // Example: tasks is an array of objects describing tasks to trigger.
+ const validPayload = {
+ tasks: [
+ {
+ taskId: 'task-123',
+ // Additional fields as required by the schema
+ },
+ ],
+ };
+
+ const response = await client.post(endpoint, validPayload);
+
+ // Check status code
+ expect(response.status).toBe(200);
+
+ // Check response headers
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Check response body structure
+ // Adjust assertions to match your actual response schema.
+ expect(typeof response.data).toBe('object');
+ // e.g., expect(response.data).toHaveProperty('success', true);
+ });
+
+ it('should trigger tasks successfully with the maximum allowed payload (up to 500 items)', async () => {
+ const client = createClient();
+
+ // Create an array of 500 items
+ const tasksArray = Array.from({ length: 500 }, (_, i) => ({ taskId: `task-${i}` }));
+ const payload = { tasks: tasksArray };
+
+ const response = await client.post(endpoint, payload);
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ // Check basic structure
+ expect(typeof response.data).toBe('object');
+ });
+
+ it('should return an error (400 or 422) if more than 500 tasks are sent', async () => {
+ const client = createClient();
+
+ // 501 tasks
+ const tasksArray = Array.from({ length: 501 }, (_, i) => ({ taskId: `task-${i}` }));
+ const payload = { tasks: tasksArray };
+
+ const response = await client.post(endpoint, payload);
+
+ const acceptableStatuses = [400, 422];
+ expect(acceptableStatuses).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should return an error (400 or 422) for an invalid payload structure', async () => {
+ const client = createClient();
+
+ // Invalid payload might be missing required fields or have wrong types.
+ // Example: missing tasks array or sending null.
+ const invalidPayload = { tasks: null };
+
+ const response = await client.post(endpoint, invalidPayload);
+
+ const acceptableStatuses = [400, 422];
+ expect(acceptableStatuses).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should return an error (400 or 422) for an empty tasks array if that is invalid', async () => {
+ const client = createClient();
+
+ // Check how your API handles an empty array.
+ const invalidPayload = { tasks: [] };
+
+ const response = await client.post(endpoint, invalidPayload);
+
+ // Depending on your API spec, it may return 200 if empty arrays are allowed.
+ // Assume here it expects at least one item.
+ const acceptableStatuses = [400, 422];
+ expect(acceptableStatuses).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should not allow unauthorized requests (401 or 403)', async () => {
+ // Create client without auth
+ const client = createClient(false);
+
+ // Minimal valid payload
+ const payload = {
+ tasks: [{ taskId: 'unauthorized-test' }],
+ };
+
+ const response = await client.post(endpoint, payload);
+
+ const unauthorizedStatuses = [401, 403];
+ expect(unauthorizedStatuses).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should handle a not found scenario (404) when the endpoint is incorrect', async () => {
+ // Sometimes you may want to test 404 by using a variant of the endpoint.
+ // For demonstration, we intentionally add a random suffix.
+
+ const client = createClient();
+ const invalidEndpoint = `${endpoint}/non-existent`;
+
+ // Minimal valid payload
+ const payload = {
+ tasks: [{ taskId: 'test-404' }],
+ };
+
+ const response = await client.post(invalidEndpoint, payload);
+
+ expect(response.status).toBe(404);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should correctly handle server errors (500) if the API triggers internal errors', async () => {
+ // This test is only valid if you have a way to trigger 500 errors.
+ // You might do that by sending some special payload that the server cannot handle.
+
+ const client = createClient();
+
+ // Potentially a payload that triggers a server error.
+ // Adjust this to your application scenario.
+ const payload = {
+ tasks: [{ taskId: 'trigger-500', causeServerError: true }],
+ };
+
+ const response = await client.post(endpoint, payload);
+
+ // We check if it returned 500 if the server encountered an internal error.
+ // If your system does not produce 500 with standard testing, skip or adapt.
+ if (response.status === 500) {
+ expect(response.headers['content-type']).toContain('application/json');
+ } else {
+ // If no 500, ensure we at least get another valid error code.
+ const acceptableStatuses = [200, 400, 422, 401, 403, 404];
+ expect(acceptableStatuses).toContain(response.status);
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.ts
new file mode 100644
index 0000000000..61fad0f7f6
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.ts
@@ -0,0 +1,151 @@
+import axios, { AxiosResponse } from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+const BASE_URL = process.env.API_BASE_URL;
+const AUTH_TOKEN = process.env.API_AUTH_TOKEN;
+
+describe('POST /api/v1/tasks/{taskIdentifier}/trigger', () => {
+ const validTaskIdentifier = '12345'; // Example valid identifier
+
+ it('should trigger the task successfully for a valid request', async () => {
+ expect(BASE_URL).toBeDefined();
+
+ try {
+ const response: AxiosResponse = await axios.post(
+ `${BASE_URL}/api/v1/tasks/${validTaskIdentifier}/trigger`,
+ {},
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: AUTH_TOKEN ? `Bearer ${AUTH_TOKEN}` : '',
+ },
+ }
+ );
+
+ // Response Validation
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ // Check the response body structure
+ expect(response.data).toHaveProperty('message');
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ throw new Error(
+ `Expected 200, but received ${error.response.status}: ${JSON.stringify(error.response.data)}`
+ );
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 400 or 422 for invalid parameters (e.g., invalid taskIdentifier)', async () => {
+ expect(BASE_URL).toBeDefined();
+
+ // Testing an invalid or empty taskIdentifier
+ const invalidIdentifier = ' ';
+
+ try {
+ await axios.post(
+ `${BASE_URL}/api/v1/tasks/${invalidIdentifier}/trigger`,
+ {},
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: AUTH_TOKEN ? `Bearer ${AUTH_TOKEN}` : '',
+ },
+ }
+ );
+ throw new Error('Expected an error (400 or 422), but request succeeded');
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ expect(error.response.headers['content-type']).toMatch(/application\/json/);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 401 or 403 for requests without a valid auth token', async () => {
+ expect(BASE_URL).toBeDefined();
+
+ try {
+ await axios.post(
+ `${BASE_URL}/api/v1/tasks/${validTaskIdentifier}/trigger`,
+ {},
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ // Purposely omit Authorization header to test unauthorized access
+ },
+ }
+ );
+ throw new Error('Expected 401 or 403, but request succeeded');
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ expect([401, 403]).toContain(error.response.status);
+ expect(error.response.headers['content-type']).toMatch(/application\/json/);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 404 when the specified taskIdentifier does not exist', async () => {
+ expect(BASE_URL).toBeDefined();
+
+ // A taskIdentifier presumed not to exist
+ const nonExistentTaskId = '99999999999';
+
+ try {
+ await axios.post(
+ `${BASE_URL}/api/v1/tasks/${nonExistentTaskId}/trigger`,
+ {},
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: AUTH_TOKEN ? `Bearer ${AUTH_TOKEN}` : '',
+ },
+ }
+ );
+ throw new Error('Expected 404, but request succeeded');
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ expect(error.response.status).toBe(404);
+ expect(error.response.headers['content-type']).toMatch(/application\/json/);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should handle server errors (e.g., 500) gracefully', async () => {
+ expect(BASE_URL).toBeDefined();
+
+ // This is conceptual. Adjust path or data to intentionally trigger a server error if possible.
+ try {
+ await axios.post(
+ `${BASE_URL}/api/v1/tasks/errorTrigger/trigger`,
+ {},
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: AUTH_TOKEN ? `Bearer ${AUTH_TOKEN}` : '',
+ },
+ }
+ );
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ if (error.response.status >= 500 && error.response.status < 600) {
+ // Validate response headers for 5xx
+ expect(error.response.headers['content-type']).toMatch(/application\/json/);
+ } else {
+ // If it is not 5xx, fail the test
+ throw new Error(`Expected 5xx, but received ${error.response.status}`);
+ }
+ } else {
+ throw error;
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.ts b/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.ts
new file mode 100644
index 0000000000..72597be6ea
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.ts
@@ -0,0 +1,140 @@
+import axios from "axios";
+import { describe, beforeAll, test, expect } from "@jest/globals";
+
+describe("Cancel a run (POST /api/v2/runs/:runId/cancel)", () => {
+ let baseURL: string;
+ let authToken: string;
+
+ beforeAll(() => {
+ // Load environment variables
+ baseURL = process.env.API_BASE_URL || "http://localhost:3000";
+ authToken = process.env.API_AUTH_TOKEN || "";
+ });
+
+ describe("Input Validation tests", () => {
+ test("Missing or empty runId should return 400 or 422", async () => {
+ const runId = "";
+ try {
+ await axios.post(
+ `${baseURL}/api/v2/runs/${runId}/cancel`,
+ {},
+ { headers: { Authorization: `Bearer ${authToken}` } }
+ );
+ } catch (error: any) {
+ // Expect either 400 or 422 for invalid payload or missing path param
+ expect([400, 422]).toContain(error.response.status);
+ expect(error.response.data).toHaveProperty("error");
+ }
+ });
+
+ test("Invalid runId format should return 400 or 422", async () => {
+ const runId = "!!!invalid-id!!!";
+ try {
+ await axios.post(
+ `${baseURL}/api/v2/runs/${runId}/cancel`,
+ {},
+ { headers: { Authorization: `Bearer ${authToken}` } }
+ );
+ } catch (error: any) {
+ // Expect either 400 or 422 for malformed runId
+ expect([400, 422]).toContain(error.response.status);
+ expect(error.response.data).toHaveProperty("error");
+ }
+ });
+ });
+
+ describe("Authorization & Authentication", () => {
+ test("No auth token should return 401 or 403", async () => {
+ const runId = "run_1234";
+ try {
+ await axios.post(`${baseURL}/api/v2/runs/${runId}/cancel`);
+ } catch (error: any) {
+ // Expect either 401 or 403 for missing token
+ expect([401, 403]).toContain(error.response.status);
+ expect(error.response.data).toHaveProperty("error");
+ }
+ });
+
+ test("Invalid auth token should return 401 or 403", async () => {
+ const runId = "run_1234";
+ try {
+ await axios.post(
+ `${baseURL}/api/v2/runs/${runId}/cancel`,
+ {},
+ { headers: { Authorization: `Bearer invalid_token` } }
+ );
+ } catch (error: any) {
+ // Expect either 401 or 403 for invalid token
+ expect([401, 403]).toContain(error.response.status);
+ expect(error.response.data).toHaveProperty("error");
+ }
+ });
+ });
+
+ describe("Response Validation", () => {
+ test("Successful cancellation should return 200 and valid schema", async () => {
+ // Assume "run_1234" is valid/in-progress or acceptable for test
+ const runId = "run_1234";
+ const response = await axios.post(
+ `${baseURL}/api/v2/runs/${runId}/cancel`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ // Response validation
+ expect(response.status).toBe(200);
+ expect(response.headers["content-type"]).toContain("application/json");
+ expect(response.data).toHaveProperty("id");
+ expect(typeof response.data.id).toBe("string");
+ });
+
+ test("Run not found should return 404", async () => {
+ // "run_not_found" is deliberately invalid to test 404
+ const runId = "run_not_found";
+ try {
+ await axios.post(
+ `${baseURL}/api/v2/runs/${runId}/cancel`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ // We expect a 404 to be thrown, so fail if no error is thrown
+ fail("Expected a 404 error but request succeeded.");
+ } catch (error: any) {
+ expect(error.response.status).toBe(404);
+ expect(error.response.data).toHaveProperty("error");
+ expect(error.response.data.error).toBe("Run not found");
+ }
+ });
+
+ test("Large runId string should return 400, 422 or 404 (implementation dependent)", async () => {
+ // Try a very long runId
+ const runId = "a".repeat(1000);
+ try {
+ await axios.post(
+ `${baseURL}/api/v2/runs/${runId}/cancel`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ } catch (error: any) {
+ // Depending on the API implementation, it might interpret long IDs differently
+ // Could be 400, 422 or 404
+ expect([400, 422, 404]).toContain(error.response.status);
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts b/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
new file mode 100644
index 0000000000..9b051ea7fb
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
@@ -0,0 +1,268 @@
+import axios, { AxiosInstance, AxiosError } from 'axios';
+import { v4 as uuidv4 } from 'uuid';
+
+/**
+ * Jest test suite for PUT /api/v1/projects/{projectRef}/envvars/{env}/{name}
+ * using axios. This suite covers:
+ * 1. Input validation (required fields, data types, edge cases)
+ * 2. Response validation (status codes, response body structure)
+ * 3. Response headers validation
+ * 4. Edge case and limit testing
+ * 5. Authorization & Authentication testing
+ */
+
+describe('PUT /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
+ let apiClient: AxiosInstance;
+ const baseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
+ const authToken = process.env.API_AUTH_TOKEN || 'INVALID_TOKEN';
+
+ // Example path parameters
+ // Adjust projectRef, env, and name accordingly for your environment.
+ const projectRef = 'testProject';
+ const env = 'dev';
+ const name = 'TEST_VAR';
+
+ // Construct the endpoint
+ const endpoint = `${baseUrl}/api/v1/projects/${projectRef}/envvars/${env}/${name}`;
+
+ // Helper function to verify if an error status code is either 400 or 422
+ function expectClientError(status: number) {
+ expect([400, 422]).toContain(status);
+ }
+
+ // Helper function to verify if an error status code is either 401 or 403
+ function expectAuthError(status: number) {
+ expect([401, 403]).toContain(status);
+ }
+
+ beforeAll(() => {
+ // Create an Axios instance with common configuration
+ apiClient = axios.create({
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+ });
+
+ /**
+ * Test Case 1:
+ * Valid request payload -> Expect 200 OK.
+ * Also validate response headers and body structure.
+ */
+ it('should update environment variable successfully with valid input', async () => {
+ // Example body. Adjust fields according to the actual OpenAPI schema.
+ // Suppose we only need a "value" field to update.
+ const validPayload = {
+ value: 'updated_value',
+ };
+
+ const response = await apiClient.put(endpoint, validPayload);
+
+ // Expect successful HTTP status (200 OK)
+ expect(response.status).toBe(200);
+
+ // Validate response headers
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate response body (assuming it follows SucceedResponse schema)
+ // Adjust these expectations based on your actual response schema
+ expect(response.data).toHaveProperty('message');
+ expect(typeof response.data.message).toBe('string');
+ });
+
+ /**
+ * Test Case 2:
+ * Invalid request payload (missing required fields, empty strings, wrong data types)
+ * Expect 400 or 422 (Client Error).
+ */
+ it('should return 400 or 422 for invalid request payload', async () => {
+ // Example invalid payload (missing "value").
+ const invalidPayload = {
+ // No 'value' field
+ };
+
+ try {
+ await apiClient.put(endpoint, invalidPayload);
+ // If it does not throw, force fail
+ fail('Expected request to fail with 400 or 422, but it succeeded');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError.response).toBeDefined();
+ if (axiosError.response) {
+ expectClientError(axiosError.response.status);
+ // Validate headers
+ expect(axiosError.response.headers['content-type']).toContain('application/json');
+ }
+ }
+ });
+
+ /**
+ * Test Case 3:
+ * Unauthorized or forbidden request (invalid token).
+ * Expect 401 or 403.
+ */
+ it('should return 401 or 403 for unauthorized or forbidden requests', async () => {
+ // Create a client with an invalid token
+ const invalidAuthClient = axios.create({
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer INVALID_TOKEN',
+ },
+ });
+
+ const payload = {
+ value: 'any_value',
+ };
+
+ try {
+ await invalidAuthClient.put(endpoint, payload);
+ fail('Expected request to fail with 401 or 403, but it succeeded');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError.response).toBeDefined();
+ if (axiosError.response) {
+ expectAuthError(axiosError.response.status);
+ // Validate headers
+ expect(axiosError.response.headers['content-type']).toContain('application/json');
+ }
+ }
+ });
+
+ /**
+ * Test Case 4:
+ * Resource not found (invalid path parameters).
+ * Expect 404.
+ */
+ it('should return 404 for non-existing resource', async () => {
+ // Use a random name to simulate a non-existent env var
+ const nonExistent = `${baseUrl}/api/v1/projects/${projectRef}/envvars/${env}/${uuidv4()}`;
+
+ const payload = {
+ value: 'any_value',
+ };
+
+ try {
+ await apiClient.put(nonExistent, payload);
+ fail('Expected request to fail with 404, but it succeeded');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError.response).toBeDefined();
+ if (axiosError.response) {
+ expect(axiosError.response.status).toBe(404);
+ // Validate headers
+ expect(axiosError.response.headers['content-type']).toContain('application/json');
+ }
+ }
+ });
+
+ /**
+ * Test Case 5:
+ * Large payload testing.
+ * Depending on your API schema, this might or might not cause errors.
+ */
+ it('should handle large payload gracefully', async () => {
+ // Create a large string for testing
+ const largeValue = 'x'.repeat(5000); // 5kB of data (adjust as needed)
+ const largePayload = {
+ value: largeValue,
+ };
+
+ try {
+ const response = await apiClient.put(endpoint, largePayload);
+ // If the API handles large payload, we expect 200 or possibly 413 if there is a limit.
+ // Adjust based on your API's defined behavior.
+ expect([200, 413]).toContain(response.status);
+ if (response.status === 200) {
+ // Validate headers
+ expect(response.headers['content-type']).toContain('application/json');
+ }
+ } catch (error) {
+ // If the server rejects large payload, it might return 400 or 413.
+ const axiosError = error as AxiosError;
+ if (axiosError.response) {
+ expect([400, 413]).toContain(axiosError.response.status);
+ } else {
+ throw error; // re-throw if response is undefined
+ }
+ }
+ });
+
+ /**
+ * Test Case 6:
+ * Missing or empty required path parameters.
+ * Expect 400 or 404 depending on API specification.
+ */
+ it('should return client error for missing or empty path parameters', async () => {
+ // Construct an invalid endpoint with empty environment and name
+ const invalidEndpoint = `${baseUrl}/api/v1/projects/${projectRef}/envvars//`;
+
+ const payload = {
+ value: 'any_value',
+ };
+
+ try {
+ await apiClient.put(invalidEndpoint, payload);
+ fail('Expected request to fail with 400, 404, or 422, but it succeeded');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError.response).toBeDefined();
+ if (axiosError.response) {
+ // Could be 400, 404, or 422 depending on API spec.
+ expect([400, 404, 422]).toContain(axiosError.response.status);
+ }
+ }
+ });
+
+ /**
+ * Test Case 7:
+ * Test authorization with valid token but insufficient privileges (if applicable)
+ * This may return 403 or something similar if role-based restrictions are in place.
+ * Skipping by default if not applicable to your API.
+ */
+ it.skip('should return 403 if the user lacks the proper role or permission (if applicable)', async () => {
+ // Create a payload
+ const payload = {
+ value: 'any_value',
+ };
+
+ try {
+ const response = await apiClient.put(endpoint, payload);
+ fail('Expected request to fail with 403, but it succeeded');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect(axiosError.response).toBeDefined();
+ if (axiosError.response) {
+ expect(axiosError.response.status).toBe(403);
+ }
+ }
+ });
+
+ /**
+ * Test Case 8:
+ * Internal server error simulation.
+ * Not always possible to trigger 500 from client side, but we can demonstrate.
+ */
+ it('should handle server errors (500) gracefully if triggered by the server', async () => {
+ // Example: some payload that might cause a server error
+ // This is highly dependent on the API. If not feasible, skip.
+ const errorPayload = {
+ value: null, // Suppose null triggers a server-side error in this hypothetical scenario.
+ };
+
+ try {
+ await apiClient.put(endpoint, errorPayload);
+ // If it succeeds, no server error was triggered.
+ // Adjust as needed if your API does not produce 500 in this scenario.
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ if (axiosError.response) {
+ // Check for 500
+ expect(axiosError.response.status).toBe(500);
+ } else {
+ // If truly no response, rethrow
+ throw error;
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.ts b/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.ts
new file mode 100644
index 0000000000..5cbe8930fe
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.ts
@@ -0,0 +1,206 @@
+import axios, { AxiosError } from "axios";
+import "dotenv/config";
+
+const baseUrl = process.env.API_BASE_URL;
+// If your token must be prefixed with "Bearer ", include that in the header below.
+// Otherwise, adjust accordingly.
+const authToken = process.env.API_AUTH_TOKEN;
+
+/**
+ * Jest test suite for the PUT /api/v1/runs/{runId}/metadata endpoint.
+ * Covers:
+ * 1. Input Validation (missing/invalid runId, metadata, etc.)
+ * 2. Response Validation (status codes, response body, etc.)
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Auth & Permission Testing
+ */
+describe("PUT /api/v1/runs/{runId}/metadata", () => {
+ // Sample IDs and payloads for testing
+ const validRunId = "123";
+ const nonExistentRunId = "999999";
+ const invalidRunId = "invalid!@#";
+
+ const validMetadataPayload = {
+ metadata: {
+ description: "This is a valid metadata description",
+ additionalData: "Some additional info"
+ }
+ };
+
+ // Missing "metadata" object
+ const invalidMetadataPayload = {
+ invalidField: "wrongField"
+ };
+
+ // Empty metadata object (edge case)
+ const emptyMetadataPayload = {
+ metadata: {}
+ };
+
+ // Large metadata payload
+ const largeMetadataPayload = {
+ metadata: {
+ largeField: "x".repeat(10000) // 10k characters
+ }
+ };
+
+ // Helper function to build URL
+ const buildUrl = (runId: string) => {
+ return `${baseUrl}/api/v1/runs/${runId}/metadata`;
+ };
+
+ // 1) Successful update with valid runId & payload
+ it("should update run metadata with valid ID & valid payload (expect 200)", async () => {
+ const url = buildUrl(validRunId);
+ try {
+ const response = await axios.put(url, validMetadataPayload, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json"
+ }
+ });
+ expect(response.status).toBe(200);
+ expect(response.data).toHaveProperty("metadata");
+ expect(typeof response.data.metadata).toBe("object");
+ expect(response.headers["content-type"]).toContain("application/json");
+ } catch (error) {
+ // For a valid scenario, we should not get here
+ throw error;
+ }
+ });
+
+ // 2) Invalid run ID (should return 400 or 422)
+ it("should return 400 or 422 for invalid runId", async () => {
+ const url = buildUrl(invalidRunId);
+ try {
+ await axios.put(url, validMetadataPayload, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json"
+ }
+ });
+ fail("Expected a 400 or 422 error, but the request succeeded.");
+ } catch (err) {
+ const error = err as AxiosError;
+ expect(error.response).toBeDefined();
+ expect([400, 422]).toContain(error.response?.status);
+ expect(error.response?.data).toHaveProperty("error");
+ expect(error.response?.headers["content-type"]).toContain("application/json");
+ }
+ });
+
+ // 3) Missing or invalid metadata in body (should return 400 or 422)
+ it("should return 400 or 422 for missing/invalid metadata", async () => {
+ const url = buildUrl(validRunId);
+ try {
+ await axios.put(url, invalidMetadataPayload, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json"
+ }
+ });
+ fail("Expected a 400 or 422 error, but the request succeeded.");
+ } catch (err) {
+ const error = err as AxiosError;
+ expect(error.response).toBeDefined();
+ expect([400, 422]).toContain(error.response?.status);
+ expect(error.response?.data).toHaveProperty("error");
+ expect(error.response?.headers["content-type"]).toContain("application/json");
+ }
+ });
+
+ // 4) Empty metadata object (valid edge case if the server allows it)
+ // Depending on API specs, this might be valid or invalid.
+ // Adjust expectation accordingly.
+ it("should handle empty metadata object (expect possibly 200 or 400/422)", async () => {
+ const url = buildUrl(validRunId);
+ try {
+ const response = await axios.put(url, emptyMetadataPayload, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json"
+ }
+ });
+ // If it is valid, we expect 200
+ expect(response.status).toBe(200);
+ expect(response.data).toHaveProperty("metadata");
+ expect(typeof response.data.metadata).toBe("object");
+ expect(response.headers["content-type"]).toContain("application/json");
+ } catch (err) {
+ const error = err as AxiosError;
+ // If the API doesn't allow empty metadata, check for 400 or 422
+ expect([400, 422]).toContain(error.response?.status);
+ expect(error.response?.data).toHaveProperty("error");
+ expect(error.response?.headers["content-type"]).toContain("application/json");
+ }
+ });
+
+ // 5) Missing or invalid authentication (should return 401 or 403)
+ it("should return 401 or 403 when auth token is missing or invalid", async () => {
+ const url = buildUrl(validRunId);
+ try {
+ await axios.put(url, validMetadataPayload, {
+ headers: {
+ // Authorization intentionally omitted
+ "Content-Type": "application/json"
+ }
+ });
+ fail("Expected a 401 or 403 error, but the request succeeded.");
+ } catch (err) {
+ const error = err as AxiosError;
+ expect(error.response).toBeDefined();
+ expect([401, 403]).toContain(error.response?.status);
+ expect(error.response?.data).toHaveProperty("error");
+ expect(error.response?.headers["content-type"]).toContain("application/json");
+ }
+ });
+
+ // 6) Non-existent run ID (should return 404)
+ it("should return 404 if the runId does not exist", async () => {
+ const url = buildUrl(nonExistentRunId);
+ try {
+ await axios.put(url, validMetadataPayload, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json"
+ }
+ });
+ fail("Expected a 404 error, but the request succeeded.");
+ } catch (err) {
+ const error = err as AxiosError;
+ expect(error.response).toBeDefined();
+ expect(error.response?.status).toBe(404);
+ expect(error.response?.data).toHaveProperty("error");
+ expect(error.response?.headers["content-type"]).toContain("application/json");
+ }
+ });
+
+ // 7) Large payload test (edge case)
+ it("should handle large metadata payload", async () => {
+ const url = buildUrl(validRunId);
+ try {
+ const response = await axios.put(url, largeMetadataPayload, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json"
+ }
+ });
+ // Depending on API limits, adjust expected outcome
+ // Assuming successful acceptance here:
+ expect(response.status).toBe(200);
+ expect(response.data).toHaveProperty("metadata");
+ expect(typeof response.data.metadata).toBe("object");
+ expect(response.headers["content-type"]).toContain("application/json");
+ } catch (err) {
+ const error = err as AxiosError;
+ // If the payload is too large, we might expect a 413 or 400
+ // or a server-specific error code.
+ // For demonstration, we handle it similarly:
+ expect([400, 413, 422, 500]).toContain(error.response?.status);
+ if (error.response) {
+ expect(error.response.headers["content-type"]).toContain("application/json");
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.ts b/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.ts
new file mode 100644
index 0000000000..b41e66cdf2
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.ts
@@ -0,0 +1,208 @@
+import axios, { AxiosError } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/**
+ * Comprehensive test suite for the Update Schedule endpoint.
+ * Endpoint: PUT /api/v1/schedules/{schedule_id}
+ * Summary: Update a schedule by its ID.
+ * Description: Update a schedule by its ID. Only works on `IMPERATIVE` schedules created in the dashboard
+ * or using imperative SDK functions (e.g. schedules.create()).
+ *
+ * Requirements:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Testing Authorization & Authentication
+ *
+ * Configuration:
+ * - API base URL is read from process.env.API_BASE_URL
+ * - Auth token is read from process.env.API_AUTH_TOKEN
+ */
+
+describe('PUT /api/v1/schedules/:schedule_id - Update Schedule', () => {
+ /**
+ * Load environment variables for API base URL and auth token.
+ * Make sure to set these variables in your environment before running the tests:
+ * - API_BASE_URL
+ * - API_AUTH_TOKEN
+ */
+ const baseURL = process.env.API_BASE_URL;
+ const authToken = process.env.API_AUTH_TOKEN;
+
+ // Replace these values with actual existing/fictitious IDs for real integration tests
+ const validScheduleId = 'valid-schedule-id';
+ const nonExistentScheduleId = 'does-not-exist';
+ const invalidScheduleId = ''; // for testing invalid ID scenarios (empty string)
+
+ // Sample valid payload (adjust fields according to your actual schema)
+ const validPayload = {
+ name: 'Updated Schedule Name',
+ description: 'This schedule has been updated.',
+ // add or remove fields based on your actual schema
+ };
+
+ // Sample invalid payload (e.g., missing required fields, wrong types, etc.)
+ const invalidPayload = {
+ name: 123, // should be a string
+ };
+
+ beforeAll(() => {
+ if (!baseURL) {
+ throw new Error('API_BASE_URL is not defined in environment variables');
+ }
+ if (!authToken) {
+ throw new Error('API_AUTH_TOKEN is not defined in environment variables');
+ }
+ });
+
+ /**
+ * Helper function to send a PUT request.
+ * @param {string} scheduleId - The schedule ID to update.
+ * @param {any} payload - The request body.
+ * @param {string | null} token - Authorization token (if any).
+ */
+ const sendPutRequest = async (scheduleId: string, payload: any, token: string | null) => {
+ const url = `${baseURL}/api/v1/schedules/${scheduleId}`;
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ }
+
+ return axios.put(url, payload, {
+ headers,
+ validateStatus: () => true, // Allow custom handling of status codes
+ });
+ };
+
+ /**
+ * 1. Happy path test - Valid token, valid scheduleId, valid payload.
+ * Expect 200 OK and a properly structured response body.
+ */
+ it('should update the schedule successfully with valid token and valid payload', async () => {
+ const response = await sendPutRequest(validScheduleId, validPayload, authToken);
+
+ // Response Validation
+ expect(response.status).toBe(200);
+
+ // Response body schema validation (trim this to your actual schema)
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('name', validPayload.name);
+ expect(response.data).toHaveProperty('description', validPayload.description);
+
+ // Headers Validation
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ /**
+ * 2. Input validation - Missing schedule ID (empty string or null).
+ * Expect 400 or 404.
+ */
+ it('should return 400 or 404 for empty schedule ID', async () => {
+ const response = await sendPutRequest(invalidScheduleId, validPayload, authToken);
+ // Could be 400 (Invalid request) or 404 (not found). Adjust as per your API behavior.
+ expect([400, 404]).toContain(response.status);
+ });
+
+ /**
+ * 3. Input validation - Non-existent schedule ID.
+ * Expect 404 Resource not found.
+ */
+ it('should return 404 if schedule does not exist', async () => {
+ const response = await sendPutRequest(nonExistentScheduleId, validPayload, authToken);
+ // Expecting 404 for a schedule that does not exist.
+ expect(response.status).toBe(404);
+ });
+
+ /**
+ * 4. Input validation - Invalid payload (wrong data types, missing required fields, etc.).
+ * Expect 400 or 422.
+ */
+ it('should return 400 or 422 for invalid payload', async () => {
+ const response = await sendPutRequest(validScheduleId, invalidPayload, authToken);
+ // The API might respond with 400 or 422 for invalid payload.
+ expect([400, 422]).toContain(response.status);
+ });
+
+ /**
+ * 5. Edge Case: Large payload.
+ * We can test by sending an extremely large string in the payload.
+ * Expect either a 200 (if the server fully supports large data) or a 400/422 if it fails validation.
+ */
+ it('should handle very large payloads appropriately', async () => {
+ const largeString = 'x'.repeat(10000); // Example large input
+ const largePayload = {
+ name: largeString,
+ description: largeString,
+ };
+
+ const response = await sendPutRequest(validScheduleId, largePayload, authToken);
+
+ // The behavior here depends on the service's handling of large data.
+ // Expect 200 if the service accepts the data; otherwise 400/422.
+ expect([200, 400, 422]).toContain(response.status);
+
+ // Check content-type header if we get a success response
+ if (response.status === 200) {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('name');
+ }
+ });
+
+ /**
+ * 6. Authorization - Missing or invalid token.
+ * Expect 401 or 403.
+ */
+ it('should return 401 or 403 for requests without a valid token', async () => {
+ const responseNoToken = await sendPutRequest(validScheduleId, validPayload, null);
+ // Expecting 401 or 403 if no valid token is sent.
+ expect([401, 403]).toContain(responseNoToken.status);
+
+ const responseInvalidToken = await sendPutRequest(validScheduleId, validPayload, 'invalid_token');
+ // Expecting 401 or 403 if an invalid token is sent.
+ expect([401, 403]).toContain(responseInvalidToken.status);
+ });
+
+ /**
+ * 7. Response Headers Validation - Confirm that Content-Type is application/json on success.
+ */
+ it('should return application/json as Content-Type on successful update', async () => {
+ const response = await sendPutRequest(validScheduleId, validPayload, authToken);
+
+ if (response.status === 200) {
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ } else {
+ // If the request didn't succeed, we check that we've handled it correctly.
+ expect([400, 401, 403, 404, 422]).toContain(response.status);
+ }
+ });
+
+ /**
+ * 8. Unexpected or server errors (500, etc.).
+ * This is harder to test deterministically, but we can handle it gracefully.
+ * Typically, we do a try/catch and see if the error is 500. Adjust as needed.
+ */
+ it('should handle 500 or other server errors gracefully (if triggered)', async () => {
+ try {
+ // Attempt to send a request that might cause a server error.
+ // This is a hypothetical scenario; typically require deeper knowledge of the API to force a 500.
+ const response = await sendPutRequest('cause-500-error', validPayload, authToken);
+ // If the API does return a 500, we check it.
+ expect([200, 400, 401, 403, 404, 422, 500]).toContain(response.status);
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ if (axiosError.response) {
+ expect(axiosError.response.status).toBe(500);
+ } else {
+ // If no response, the error might be networking-related.
+ throw error;
+ }
+ }
+ });
+});