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}.py b/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.py
new file mode 100644
index 0000000000..58734cea76
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.py
@@ -0,0 +1,159 @@
+import axios, { AxiosInstance } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/**
+ * DELETE /api/v1/projects/{projectRef}/envvars/{env}/{name}
+ * Summary: Delete environment variable
+ * Description: Delete a specific environment variable for a specific project and environment.
+ *
+ * This test suite covers:
+ * 1. Input Validation (e.g., missing or invalid path params)
+ * 2. Response Validation (e.g., correct status codes and JSON schema checks)
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing (e.g., extremely long params)
+ * 5. Authorization & Authentication Tests
+ */
+
+describe('DELETE /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
+ let request: AxiosInstance;
+
+ // Define some valid test data
+ const validProjectRef = 'exampleProjectRef';
+ const validEnv = 'staging';
+ const validName = 'EXAMPLE_VAR';
+
+ beforeAll(() => {
+ // Create an Axios instance for all requests in this suite
+ // Loads base URL and auth token from environment variables
+ request = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`,
+ },
+ validateStatus: () => true, // we'll handle status codes manually in each test
+ });
+ });
+
+ /**
+ * 1. Valid Request
+ */
+ describe('Valid Request', () => {
+ it('should delete the environment variable successfully (expect 200)', async () => {
+ // Perform the DELETE request with valid path parameters
+ const response = await request.delete(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${validName}`
+ );
+
+ // Check valid status code
+ expect(response.status).toBe(200);
+
+ // Validate response headers
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Validate response body structure (SucceedResponse)
+ // Example schema might have { message: 'Environment variable deleted successfully' }
+ expect(response.data).toHaveProperty('message');
+ expect(typeof response.data.message).toBe('string');
+ });
+ });
+
+ /**
+ * 2. Input Validation (Invalid or Malformed Parameters)
+ */
+ describe('Input Validation', () => {
+ it('should return 400 or 422 when path param is empty or malformed', async () => {
+ // Using an empty name to force an invalid path param scenario
+ const invalidName = '';
+
+ const response = await request.delete(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${invalidName}`
+ );
+
+ // The API could return either 400 or 422 for invalid parameters
+ expect([400, 422]).toContain(response.status);
+
+ // Validate response headers
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Validate error response body (ErrorResponse)
+ // Example schema might have { error: 'some error message' }
+ expect(response.data).toHaveProperty('error');
+ expect(typeof response.data.error).toBe('string');
+ });
+
+ it('should return 400 or 422 when path param is extremely long', async () => {
+ // Large string to test boundary condition
+ const longName = 'A'.repeat(1000);
+
+ const response = await request.delete(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${longName}`
+ );
+
+ // The API could return either 400 or 422 for invalid parameters
+ expect([400, 422]).toContain(response.status);
+
+ // Validate response headers
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Validate error response body (ErrorResponse)
+ expect(response.data).toHaveProperty('error');
+ expect(typeof response.data.error).toBe('string');
+ });
+ });
+
+ /**
+ * 3. Authorization & Authentication
+ */
+ describe('Authorization & Authentication', () => {
+ it('should return 401 or 403 if the token is missing or invalid', async () => {
+ // Create another Axios instance without Authorization header
+ const unauthRequest = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ validateStatus: () => true,
+ });
+
+ const response = await unauthRequest.delete(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${validName}`
+ );
+
+ // Expect 401 (Unauthorized) or 403 (Forbidden)
+ expect([401, 403]).toContain(response.status);
+
+ // Validate response headers
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Validate error response structure
+ expect(response.data).toHaveProperty('error');
+ expect(typeof response.data.error).toBe('string');
+ });
+ });
+
+ /**
+ * 4. Resource Not Found
+ */
+ describe('Resource Not Found', () => {
+ it('should return 404 if the specified environment variable does not exist', async () => {
+ const nonExistentName = 'NON_EXISTENT_VAR';
+
+ const response = await request.delete(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/${nonExistentName}`
+ );
+
+ // Expect 404 for missing resource
+ expect(response.status).toBe(404);
+
+ // Validate response headers
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Validate error response body
+ expect(response.data).toHaveProperty('error');
+ expect(typeof response.data.error).toBe('string');
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.py b/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.py
new file mode 100644
index 0000000000..1707a54700
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.py
@@ -0,0 +1,190 @@
+import axios, { AxiosInstance } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/**
+ * This test suite validates the DELETE /api/v1/schedules/{schedule_id} endpoint.
+ * It covers:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Cases & Limit Testing
+ * 5. Authorization & Authentication
+ *
+ * Requirements:
+ * - You must have API_BASE_URL and API_AUTH_TOKEN set in your environment variables.
+ * - The endpoint is DELETE /api/v1/schedules/{schedule_id}
+ * - A valid schedule_id corresponds to an IMPERATIVE schedule that actually exists.
+ * - The endpoint may respond with 400 or 422 for invalid input.
+ * - The endpoint may respond with 401 or 403 for unauthorized or forbidden access.
+ * - The endpoint may respond with 404 if the resource is not found.
+ */
+
+describe('DELETE /api/v1/schedules/{schedule_id}', () => {
+ let apiClient: AxiosInstance;
+ let baseURL = '';
+ let authToken = '';
+
+ beforeAll(() => {
+ // Load environment variables
+ baseURL = process.env.API_BASE_URL || '';
+ authToken = process.env.API_AUTH_TOKEN || '';
+
+ // Create an Axios instance
+ apiClient = axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ validateStatus: () => true, // We'll manually check status codes to handle multiple possible outcomes
+ });
+ });
+
+ // Utility function to get the Authorization header
+ function getAuthHeaders(token?: string) {
+ if (token) {
+ return { Authorization: `Bearer ${token}` };
+ }
+ if (authToken) {
+ return { Authorization: `Bearer ${authToken}` };
+ }
+ return {};
+ }
+
+ /**
+ * Note on test data:
+ * - Adjust these IDs to match real data in your test environment.
+ * - "validScheduleId" should be an existing imperative schedule ID.
+ * - "nonExistentScheduleId" doesn't exist, triggering a 404.
+ * - "invalidScheduleId" simulates malformed input.
+ */
+ const validScheduleId = 'validScheduleId';
+ const nonExistentScheduleId = 'nonExistentScheduleId';
+ const invalidScheduleId = ''; // Empty string or any invalid type
+
+ /******************
+ * 1) Input Validation
+ ******************/
+ describe('Input Validation', () => {
+ it('should return 404 or 400/422 when schedule_id is missing in the path', async () => {
+ // Some servers might interpret a missing ID as a 404 (route not found)
+ // Others might throw 400/422 if the path was recognized but the ID was empty.
+ const response = await apiClient.delete('/api/v1/schedules/', {
+ headers: getAuthHeaders(),
+ });
+ expect([400, 404, 422]).toContain(response.status);
+ });
+
+ it('should return 400 or 422 if schedule_id is invalid (empty string)', async () => {
+ const response = await apiClient.delete(`/api/v1/schedules/${invalidScheduleId}`, {
+ headers: getAuthHeaders(),
+ });
+ // Depending on implementation, could be 400, 422, or even 404.
+ expect([400, 404, 422]).toContain(response.status);
+ });
+
+ // If your API expects strictly string type for schedule_id, you can add more tests for numeric or other malformed types.
+ });
+
+ /******************
+ * 2) Response Validation
+ ******************/
+ describe('Response Validation', () => {
+ it('should delete the schedule and return 200 for a valid existing schedule_id', async () => {
+ // This test assumes the validScheduleId is a real schedule.
+ // If the schedule is not truly existing, the test may fail or return 404.
+ // Ensure validScheduleId references an actual IMPERATIVE schedule.
+ const response = await apiClient.delete(`/api/v1/schedules/${validScheduleId}`, {
+ headers: getAuthHeaders(),
+ });
+
+ // We expect 200 for a successful deletion.
+ expect(response.status).toBe(200);
+ // Optionally, if response has a body, we can validate the schema.
+ // Example: expect(response.data).toHaveProperty('message', 'Schedule deleted successfully');
+ });
+
+ it('should return 404 if the schedule_id does not exist', async () => {
+ const response = await apiClient.delete(`/api/v1/schedules/${nonExistentScheduleId}`, {
+ headers: getAuthHeaders(),
+ });
+
+ expect(response.status).toBe(404);
+ // Optionally check response body if the API returns an error message.
+ // Example: expect(response.data).toHaveProperty('error', 'Resource not found');
+ });
+ });
+
+ /******************
+ * 3) Response Headers Validation
+ ******************/
+ describe('Response Headers Validation', () => {
+ it('should return application/json in Content-Type header under normal conditions', async () => {
+ const response = await apiClient.delete(`/api/v1/schedules/${validScheduleId}`, {
+ headers: getAuthHeaders(),
+ });
+
+ // Some APIs only return a body for certain status codes. If there's no body, it can differ.
+ // But typically for a JSON-based API, we expect "application/json".
+ const contentType = response.headers['content-type'] || '';
+ expect(contentType).toContain('application/json');
+ });
+ });
+
+ /******************
+ * 4) Edge Cases & Limit Testing
+ ******************/
+ describe('Edge Cases & Limit Testing', () => {
+ it('should handle unauthorized requests (missing token) with 401 or 403', async () => {
+ // Calling without Authorization header
+ const response = await apiClient.delete(`/api/v1/schedules/${validScheduleId}`);
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should handle requests with an invalid token with 401 or 403', async () => {
+ const response = await apiClient.delete(`/api/v1/schedules/${validScheduleId}`, {
+ headers: getAuthHeaders('invalidToken'),
+ });
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should handle server errors (5xx) gracefully if the server triggers such an error', async () => {
+ // This test is hypothetical. The actual occurrence might require a special setup.
+ // We simply show that we expect 5xx if something goes wrong on the server.
+ // In practice, you could mock or simulate an internal server error.
+ // Here we just illustrate how to handle if it occurs.
+
+ // Example pseudo-code:
+ // const response = await apiClient.delete(`/api/v1/schedules/specialCaseIdThatCausesError`, {
+ // headers: getAuthHeaders(),
+ // });
+
+ // For demonstration, we won't actually throw a server error; just a placeholder.
+ // If you had a special route that triggers a 500, you could test it.
+ // expect(response.status).toBe(500);
+ });
+ });
+
+ /******************
+ * 5) Testing Authorization & Authentication
+ ******************/
+ describe('Authorization & Authentication', () => {
+ it('should delete a schedule successfully when authorized', async () => {
+ // Re-using validScheduleId, ensuring token is provided.
+ const response = await apiClient.delete(`/api/v1/schedules/${validScheduleId}`, {
+ headers: getAuthHeaders(),
+ });
+
+ // 200 is expected if the schedule exists, 404 if it was already deleted.
+ // Some implementations might return 204 No Content for a successful delete.
+ expect([200, 404]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 if token is expired or invalid', async () => {
+ const response = await apiClient.delete(`/api/v1/schedules/${validScheduleId}`, {
+ headers: getAuthHeaders('expiredOrInvalidToken'),
+ });
+
+ expect([401, 403]).toContain(response.status);
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.py b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.py
new file mode 100644
index 0000000000..e7f1227589
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.py
@@ -0,0 +1,123 @@
+import axios, { AxiosInstance, AxiosError } from 'axios';
+
+describe('GET /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
+ let apiClient: AxiosInstance;
+
+ // Example valid references for the happy path
+ const existingProjectRef = 'myProject';
+ const existingEnv = 'production';
+ const existingVarName = 'SOME_VAR';
+
+ // Example invalid or non-existent references
+ const nonExistentVarName = 'NON_EXISTENT_VAR';
+ const invalidProjectRef = '';
+ const invalidEnv = '';
+ const invalidName = '';
+
+ beforeAll(() => {
+ // Create an Axios instance using environment variables
+ const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ const token = process.env.API_AUTH_TOKEN || ''; // If empty, tests for missing token will apply
+
+ apiClient = axios.create({
+ baseURL,
+ headers: {
+ // Only attach Authorization header if token exists
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ });
+ });
+
+ // 1. Successful retrieval (200)
+ test('should retrieve environment variable successfully (200)', async () => {
+ const url = `/api/v1/projects/${existingProjectRef}/envvars/${existingEnv}/${existingVarName}`;
+
+ const response = await apiClient.get(url);
+
+ // Response validation
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ expect(response.data).toBeDefined();
+
+ // Basic response body validation (assuming EnvVarValue has 'name' and 'value')
+ expect(response.data).toHaveProperty('name');
+ expect(typeof response.data.name).toBe('string');
+ expect(response.data).toHaveProperty('value');
+ expect(typeof response.data.value).toBe('string');
+ });
+
+ // 2. Resource not found (404)
+ test('should return 404 for a non-existing environment variable', async () => {
+ const url = `/api/v1/projects/${existingProjectRef}/envvars/${existingEnv}/${nonExistentVarName}`;
+
+ try {
+ await apiClient.get(url);
+ fail('Expected 404 Not Found');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ expect(err.response?.status).toBe(404);
+ expect(err.response?.data).toBeDefined();
+ // Example check for a standard error response field
+ expect(err.response?.data).toHaveProperty('message');
+ }
+ });
+
+ // 3. Invalid path parameters (400 or 422)
+ test('should return 400 or 422 for invalid path params (empty projectRef)', async () => {
+ const url = `/api/v1/projects/${invalidProjectRef}/envvars/${existingEnv}/${existingVarName}`;
+
+ try {
+ await apiClient.get(url);
+ fail('Expected 400 or 422 error');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ expect([400, 422]).toContain(err.response?.status);
+ }
+ });
+
+ test('should return 400 or 422 for invalid path params (empty env)', async () => {
+ const url = `/api/v1/projects/${existingProjectRef}/envvars/${invalidEnv}/${existingVarName}`;
+
+ try {
+ await apiClient.get(url);
+ fail('Expected 400 or 422 error');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ expect([400, 422]).toContain(err.response?.status);
+ }
+ });
+
+ test('should return 400 or 422 for invalid path params (empty name)', async () => {
+ const url = `/api/v1/projects/${existingProjectRef}/envvars/${existingEnv}/${invalidName}`;
+
+ try {
+ await apiClient.get(url);
+ fail('Expected 400 or 422 error');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ expect([400, 422]).toContain(err.response?.status);
+ }
+ });
+
+ // 4. Unauthorized / forbidden requests (401 or 403)
+ test('should return 401 or 403 for missing auth token', async () => {
+ // Create a client with no auth header
+ const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ const clientWithoutAuth = axios.create({ baseURL });
+
+ const url = `/api/v1/projects/${existingProjectRef}/envvars/${existingEnv}/${existingVarName}`;
+
+ try {
+ await clientWithoutAuth.get(url);
+ fail('Expected 401 or 403 error');
+ } catch (error) {
+ const err = error as AxiosError;
+ expect(err.response).toBeDefined();
+ expect([401, 403]).toContain(err.response?.status);
+ }
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.py b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.py
new file mode 100644
index 0000000000..58418295d9
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.py
@@ -0,0 +1,134 @@
+import axios, { AxiosError } from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+/**
+ * Jest test suite for GET /api/v1/projects/{projectRef}/envvars/{env}
+ * This suite validates:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Cases & Limits
+ * 5. Authorization & Authentication
+ */
+describe('GET /api/v1/projects/{projectRef}/envvars/{env}', () => {
+ const baseURL = process.env.API_BASE_URL;
+ const token = process.env.API_AUTH_TOKEN;
+
+ // Helper to build authorization headers
+ const getAuthHeaders = (authToken?: string) => {
+ return {
+ headers: {
+ Authorization: `Bearer ${authToken || token}`,
+ 'Content-Type': 'application/json',
+ },
+ };
+ };
+
+ it('should return 200 and a valid response for a valid request', async () => {
+ const projectRef = 'validProject';
+ const env = 'dev';
+ const url = `${baseURL}/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ try {
+ const response = await axios.get(url, getAuthHeaders());
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Basic schema validation (assuming response contains an "envVars" array)
+ expect(response.data).toHaveProperty('envVars');
+ expect(Array.isArray(response.data.envVars)).toBe(true);
+ } catch (err) {
+ const axiosError = err as AxiosError;
+ fail(`Expected 2xx response, but got: ${axiosError.message}`);
+ }
+ });
+
+ it('should return 400 or 404 when the projectRef is missing', async () => {
+ const projectRef = '';
+ const env = 'dev';
+ const url = `${baseURL}/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ try {
+ await axios.get(url, getAuthHeaders());
+ fail('Expected a 400 or 404 error, but request succeeded.');
+ } catch (err) {
+ const axiosError = err as AxiosError;
+ // Depending on the API, this may be 400 (bad request) or 404 (not found)
+ expect([400, 404]).toContain(axiosError.response?.status);
+ expect(axiosError.response?.headers['content-type']).toMatch(/application\/json/i);
+ }
+ });
+
+ it('should return 400 or 422 when the projectRef contains invalid characters', async () => {
+ const projectRef = '???!!!';
+ const env = 'dev';
+ const url = `${baseURL}/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ try {
+ await axios.get(url, getAuthHeaders());
+ fail('Expected a 400/422 error, but request succeeded.');
+ } catch (err) {
+ const axiosError = err as AxiosError;
+ // Some APIs might respond with 400 or 422 for invalid inputs
+ expect([400, 422]).toContain(axiosError.response?.status);
+ expect(axiosError.response?.headers['content-type']).toMatch(/application\/json/i);
+ }
+ });
+
+ it('should return 401 or 403 when the request is unauthorized', async () => {
+ const projectRef = 'validProject';
+ const env = 'dev';
+ const url = `${baseURL}/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ try {
+ // Passing an invalid or missing token
+ await axios.get(url, getAuthHeaders('invalidOrMissingToken'));
+ fail('Expected a 401 or 403 error, but request succeeded.');
+ } catch (err) {
+ const axiosError = err as AxiosError;
+ // API might respond with 401 or 403
+ expect([401, 403]).toContain(axiosError.response?.status);
+ expect(axiosError.response?.headers['content-type']).toMatch(/application\/json/i);
+ }
+ });
+
+ it('should return 400 or 404 for an excessively large projectRef', async () => {
+ // Testing boundary/limit conditions on projectRef length
+ const largeProjectRef = 'a'.repeat(1000);
+ const env = 'dev';
+ const url = `${baseURL}/api/v1/projects/${largeProjectRef}/envvars/${env}`;
+
+ try {
+ await axios.get(url, getAuthHeaders());
+ fail('Expected a 400 or 404 error for large projectRef, but request succeeded.');
+ } catch (err) {
+ const axiosError = err as AxiosError;
+ expect([400, 404]).toContain(axiosError.response?.status);
+ expect(axiosError.response?.headers['content-type']).toMatch(/application\/json/i);
+ }
+ });
+
+ it('should handle no environment variables found (empty list or 404)', async () => {
+ // This tests the scenario where the project/env is valid but no env vars exist
+ const projectRef = 'projectWithNoEnvVars';
+ const env = 'dev';
+ const url = `${baseURL}/api/v1/projects/${projectRef}/envvars/${env}`;
+
+ try {
+ const response = await axios.get(url, getAuthHeaders());
+ // This may return 200 with an empty array, or 404 if resource does not exist
+ expect([200, 404]).toContain(response.status);
+
+ if (response.status === 200) {
+ expect(response.data).toHaveProperty('envVars');
+ expect(Array.isArray(response.data.envVars)).toBe(true);
+ // Expect empty array if no env vars are found
+ expect(response.data.envVars.length).toBe(0);
+ }
+ } catch (err) {
+ const axiosError = err as AxiosError;
+ // If the API strictly returns 404 for missing envvars
+ expect([404]).toContain(axiosError.response?.status);
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.py b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.py
new file mode 100644
index 0000000000..34a88676bc
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.py
@@ -0,0 +1 @@
+import axios, { AxiosInstance, AxiosError } from 'axios';\nimport { describe, it, expect, beforeAll } from '@jest/globals';\n\ndescribe('GET /api/v1/projects/{projectRef}/runs - List project runs', () => {\n let client: AxiosInstance;\n const validProjectRef = 'my-valid-project';\n const invalidProjectRef = 'non-existing-project';\n\n beforeAll(() => {\n // Create an Axios instance with the base URL and authorization header.\n // These values are loaded from environment variables (API_BASE_URL and API_AUTH_TOKEN).\n client = axios.create({\n baseURL: process.env.API_BASE_URL,\n headers: {\n Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`\n }\n });\n });\n\n it('should return 200 and a valid response when called with correct parameters', async () => {\n const params = {\n env: 'production',\n status: 'success',\n limit: 5\n };\n\n const response = await client.get(`/api/v1/projects/${validProjectRef}/runs`, { params });\n\n // 1. Response status and headers\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n\n // 2. Basic validation of the response body\n // This should match #/components/schemas/ListRunsResult in principle.\n expect(response.data).toHaveProperty('runs');\n expect(Array.isArray(response.data.runs)).toBe(true);\n });\n\n it('should return 400 (or 422) for invalid query parameter type (e.g., limit=abc)', async () => {\n const params = { limit: 'abc' };\n\n try {\n await client.get(`/api/v1/projects/${validProjectRef}/runs`, { params });\n throw new Error('Request should have failed due to invalid parameter type');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n // The API could return 400 or 422 for invalid request payloads.\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should return 401 (or 403) if no auth token is provided', async () => {\n const unauthClient = axios.create({\n baseURL: process.env.API_BASE_URL\n });\n\n try {\n await unauthClient.get(`/api/v1/projects/${validProjectRef}/runs`);\n throw new Error('Request should have failed due to missing token');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n // The API may return 401 or 403 in these cases.\n expect([401, 403]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should return 404 if the projectRef does not exist', async () => {\n try {\n await client.get(`/api/v1/projects/${invalidProjectRef}/runs`);\n throw new Error('Request should have failed due to invalid projectRef');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect(axiosError.response.status).toBe(404);\n } else {\n throw error;\n }\n }\n });\n\n it('should return an empty list if no runs are found', async () => {\n // Provide parameters that are unlikely to match any run\n const params = { status: 'does-not-exist-123' };\n const response = await client.get(`/api/v1/projects/${validProjectRef}/runs`, { params });\n\n expect(response.status).toBe(200);\n expect(response.data).toHaveProperty('runs');\n expect(response.data.runs.length).toBe(0);\n });\n\n it('should handle cursor pagination parameters correctly', async () => {\n // Example of testing pagination-related parameters (cursorPagination).\n const params = { limit: 1 };\n const response = await client.get(`/api/v1/projects/${validProjectRef}/runs`, { params });\n\n expect(response.status).toBe(200);\n expect(Array.isArray(response.data.runs)).toBe(true);\n // Here, you might also check for 'nextCursor', 'prevCursor', etc., if defined in the schema.\n });\n\n it('should return 400 (or 422) for malformed or out-of-range date filters', async () => {\n // Assuming runsFilterWithEnv allows date filters, e.g. createdAtBefore, createdAtAfter.\n const params = { createdAtBefore: 'not-a-date' };\n\n try {\n await client.get(`/api/v1/projects/${validProjectRef}/runs`, { params });\n throw new Error('Request should have failed due to invalid date');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n // The API could return 400 or 422 for invalid request payloads.\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n});\n
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.py b/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.py
new file mode 100644
index 0000000000..28618733e0
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.py
@@ -0,0 +1 @@
+import axios, { AxiosInstance, AxiosResponse } from 'axios';\nimport { describe, it, expect, beforeAll } from '@jest/globals';\n\ndescribe('GET /api/v1/runs', () => {\n let axiosInstance: AxiosInstance;\n const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';\n const validAuthToken = process.env.API_AUTH_TOKEN || 'valid-auth-token';\n\n beforeAll(() => {\n axiosInstance = axios.create({\n baseURL,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n });\n\n it('should return 200 OK with no filters (happy path)', async () => {\n const response: AxiosResponse = await axiosInstance.get('/api/v1/runs', {\n headers: { Authorization: `Bearer ${validAuthToken}` },\n params: {},\n });\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n\n // Basic schema check (assuming response body matches ListRunsResult)\n // Here, we assume the response has a 'runs' array and possibly 'pagination' info.\n expect(response.data).toHaveProperty('runs');\n expect(Array.isArray(response.data.runs)).toBe(true);\n });\n\n it('should return 200 OK with valid filters', async () => {\n // Example of a valid filter (status, created at, etc.)\n // Adjust the query parameters according to the actual filters allowed by your API.\n const response: AxiosResponse = await axiosInstance.get('/api/v1/runs', {\n headers: { Authorization: `Bearer ${validAuthToken}` },\n params: {\n status: 'completed',\n // We can add more valid filters as needed\n },\n });\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n expect(response.data).toHaveProperty('runs');\n expect(Array.isArray(response.data.runs)).toBe(true);\n });\n\n it('should return 400 or 422 for invalid parameter data type', async () => {\n // E.g., passing a numeric parameter where a string is expected\n // or a negative page size, etc.\n try {\n await axiosInstance.get('/api/v1/runs', {\n headers: { Authorization: `Bearer ${validAuthToken}` },\n params: { status: 123 }, // Invalid: expecting a string filter, but sending a number\n });\n // If the request does not throw, fail the test\n fail('Expected request to fail with 400 or 422, but it succeeded.');\n } catch (error: any) {\n const status = error.response?.status;\n // Check if status is 400 or 422 as mentioned in the instructions\n expect([400, 422]).toContain(status);\n expect(error.response?.headers['content-type']).toContain('application/json');\n }\n });\n\n it('should return 401 or 403 for unauthorized request', async () => {\n const invalidAuthToken = 'invalid-auth-token';\n try {\n await axiosInstance.get('/api/v1/runs', {\n headers: { Authorization: `Bearer ${invalidAuthToken}` },\n });\n fail('Expected request to fail with 401 or 403, but it succeeded.');\n } catch (error: any) {\n const status = error.response?.status;\n // According to the specs, it can be 401 or 403 if auth is invalid\n expect([401, 403]).toContain(status);\n }\n });\n\n it('should return 200 OK with an empty array if no runs are found', async () => {\n // Passing filters that are guaranteed to yield zero results (example)\n const response: AxiosResponse = await axiosInstance.get('/api/v1/runs', {\n headers: { Authorization: `Bearer ${validAuthToken}` },\n params: { status: 'this-status-does-not-exist' },\n });\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n expect(response.data).toHaveProperty('runs');\n expect(Array.isArray(response.data.runs)).toBe(true);\n expect(response.data.runs.length).toBe(0);\n });\n\n it('should handle large pagination values', async () => {\n // Example of testing a large limit or page, if supported by the API\n // The #/components/parameters/cursorPagination might have a limit or page.\n const largePage = Number.MAX_SAFE_INTEGER;\n try {\n const response: AxiosResponse = await axiosInstance.get('/api/v1/runs', {\n headers: { Authorization: `Bearer ${validAuthToken}` },\n params: { page: largePage },\n });\n\n // Some APIs might return 400 for out-of-range pages.\n expect([200, 400, 422]).toContain(response.status);\n } catch (error: any) {\n // If an error is thrown, verify it is in the acceptable set (400 or 422).\n const status = error.response?.status;\n expect([400, 422]).toContain(status);\n }\n });\n\n it('should validate the response headers (e.g., Content-Type)', async () => {\n const response: AxiosResponse = await axiosInstance.get('/api/v1/runs', {\n headers: { Authorization: `Bearer ${validAuthToken}` },\n });\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n\n // Check additional headers if needed (e.g., Cache-Control, X-RateLimit, etc.)\n // For example:\n // expect(response.headers['cache-control']).toBeDefined();\n // expect(response.headers['x-ratelimit-limit']).toBeDefined();\n });\n});\n
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.py b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.py
new file mode 100644
index 0000000000..80c555a84a
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.py
@@ -0,0 +1,179 @@
+import axios, { AxiosInstance } from "axios";
+import { AxiosResponse } from "axios";
+import dotenv from "dotenv";
+
+dotenv.config();
+
+/**
+ * Example interface for the expected schedule response.
+ * Adjust fields as necessary to match your actual schema.
+ */
+interface Schedule {
+ id: string;
+ name?: string;
+ // Add other fields that exist in your #/components/schemas/ScheduleObject
+}
+
+/**
+ * Jest test suite for GET /api/v1/schedules/{schedule_id}.
+ */
+describe("GET /api/v1/schedules/{schedule_id} - Retrieve Schedule", () => {
+ let client: AxiosInstance;
+
+ beforeAll(() => {
+ client = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`,
+ },
+ validateStatus: () => true, // We'll manually check status codes in each test
+ });
+ });
+
+ describe("Valid requests", () => {
+ test("should retrieve a schedule (200) when provided a valid schedule_id", async () => {
+ // Replace with a valid schedule ID that exists in your test environment
+ const validScheduleId = "sched_1234";
+
+ const response: AxiosResponse = await client.get(
+ `/api/v1/schedules/${validScheduleId}`
+ );
+
+ // Response Validation
+ expect([200]).toContain(response.status);
+ expect(response.headers["content-type"]).toMatch(/application\/json/i);
+
+ // Basic shape check (customize according to your schema)
+ const data = response.data as Schedule;
+ expect(data).toBeDefined();
+ expect(typeof data.id).toBe("string");
+ // Add more property checks if needed
+ });
+ });
+
+ describe("Input Validation", () => {
+ test("should return 400 or 422 when schedule_id is invalid (e.g., malformed)", async () => {
+ // For example, an ID that clearly doesn't match your format
+ const malformedScheduleId = "!!!";
+ const response: AxiosResponse = await client.get(
+ `/api/v1/schedules/${malformedScheduleId}`
+ );
+
+ expect([400, 422]).toContain(response.status);
+ });
+
+ test("should return 404 when schedule_id does not exist", async () => {
+ // Non-existent but validly-formatted schedule_id
+ const nonExistentId = "sched_non_existent";
+ const response: AxiosResponse = await client.get(
+ `/api/v1/schedules/${nonExistentId}`
+ );
+
+ expect([404]).toContain(response.status);
+ });
+
+ test("should handle edge case with extremely large schedule_id (potentially 400 or 422)", async () => {
+ // Make a very long string to test boundary conditions
+ const largeScheduleId = "sched_" + "x".repeat(1000);
+ const response: AxiosResponse = await client.get(
+ `/api/v1/schedules/${largeScheduleId}`
+ );
+
+ // Depending on your API, it might return 400, 422, or possibly 404
+ expect([400, 422, 404]).toContain(response.status);
+ });
+
+ test("should return error (400/422) when schedule_id is empty", async () => {
+ // An empty schedule_id might cause a route parameter error or 404
+ const response: AxiosResponse = await client.get("/api/v1/schedules/");
+
+ // Many frameworks might treat this as a 404 if the route doesn't match
+ // or 400 if the framework explicitly validates the path param
+ expect([400, 404, 422]).toContain(response.status);
+ });
+ });
+
+ describe("Authorization & Authentication", () => {
+ test("should return 401 or 403 when no auth token is provided", async () => {
+ // Create a client without Authorization header
+ const unauthClient = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ validateStatus: () => true,
+ });
+
+ const response: AxiosResponse = await unauthClient.get(
+ "/api/v1/schedules/sched_1234"
+ );
+
+ expect([401, 403]).toContain(response.status);
+ });
+
+ test("should return 401 or 403 when invalid auth token is provided", async () => {
+ // Create a client with an invalid token
+ const invalidAuthClient = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ Authorization: "Bearer invalid_token",
+ },
+ validateStatus: () => true,
+ });
+
+ const response: AxiosResponse = await invalidAuthClient.get(
+ "/api/v1/schedules/sched_1234"
+ );
+
+ expect([401, 403]).toContain(response.status);
+ });
+ });
+
+ describe("Response Headers Validation", () => {
+ test("should return Content-Type as application/json on success", async () => {
+ const validScheduleId = "sched_1234";
+ const response: AxiosResponse = await client.get(
+ `/api/v1/schedules/${validScheduleId}`
+ );
+
+ if (response.status === 200) {
+ expect(response.headers["content-type"]).toMatch(/application\/json/i);
+ }
+ });
+ });
+
+ describe("Edge Cases & Error Handling", () => {
+ test("should handle unexpected server errors (5xx) gracefully if they occur", async () => {
+ // For demonstration, this test is conceptual.
+ // If you have a way to trigger a server error, add that scenario.
+ // Otherwise, you might mock or intercept the request.
+
+ // We'll attempt to call a route that might produce 500
+ const response: AxiosResponse = await client.get(
+ "/api/v1/schedules/trigger_500_error"
+ );
+
+ if (response.status >= 500 && response.status < 600) {
+ expect(response.data).toBeDefined();
+ } else {
+ // The test won't fail if the server doesn't actually produce 5xx.
+ // Adjust as necessary for your environment.
+ expect([200, 400, 404, 422, 401, 403]).toContain(response.status);
+ }
+ });
+
+ test("rate limiting test (429) if applicable", async () => {
+ // This is a placeholder if your API enforces rate limits.
+ // Typically you'd spam requests in a loop to trigger 429.
+ // Pseudocode provided here.
+
+ // for (let i = 0; i < 1000; i++) {
+ // await client.get("/api/v1/schedules/sched_1234");
+ // }
+ // Then check one last call's response.
+
+ // const response: AxiosResponse = await client.get("/api/v1/schedules/sched_1234");
+ // expect([429, 200]).toContain(response.status);
+
+ // For now, we'll just do a placeholder expectation.
+ expect(true).toBe(true);
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.py b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.py
new file mode 100644
index 0000000000..9550e20bcd
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.py
@@ -0,0 +1,188 @@
+import axios, { AxiosResponse } from 'axios';
+import "dotenv/config";
+
+/***************************************************************************************
+ * Jest test suite for GET /api/v1/schedules
+ *
+ * This test suite covers:
+ * 1. Input validation (query parameters)
+ * 2. Response validation (status code, schema structure)
+ * 3. Response headers validation
+ * 4. Edge cases & limit testing
+ * 5. Authentication & authorization scenarios
+ ***************************************************************************************/
+
+describe('GET /api/v1/schedules', () => {
+ // Base API URL and authorization token from environment variables
+ const baseURL = process.env.API_BASE_URL || '';
+ const authToken = process.env.API_AUTH_TOKEN || '';
+
+ // Create a reusable Axios instance pre-configured with base URL & headers
+ const axiosInstance = axios.create({
+ baseURL,
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ validateStatus: () => true, // Let us handle status code checks manually
+ });
+
+ // Helper function to check if a response has JSON content-type
+ const expectJsonContentType = (headers: Record): void => {
+ expect(headers['content-type']).toContain('application/json');
+ };
+
+ // 1. Happy path: valid parameters
+ it('should return 200 and a list of schedules with valid optional query parameters', async () => {
+ const response: AxiosResponse = await axiosInstance.get('/api/v1/schedules', {
+ params: {
+ page: 1,
+ perPage: 10,
+ },
+ });
+
+ // Check status
+ expect([200]).toContain(response.status);
+
+ // Check headers
+ expectJsonContentType(response.headers);
+
+ // Basic schema checks (assuming the response should have { data: [...], meta: {...} })
+ expect(response.data).toBeDefined();
+ expect(response.data).toHaveProperty('data');
+ expect(response.data).toHaveProperty('meta');
+ expect(Array.isArray(response.data.data)).toBe(true);
+
+ // Additional checks: page/perPage if returned, etc.
+ // Example: expect(response.data.meta.page).toBe(1);
+ });
+
+ // 2. Missing optional query parameters (should still succeed)
+ it('should return 200 when query parameters are omitted', async () => {
+ const response: AxiosResponse = await axiosInstance.get('/api/v1/schedules');
+
+ // Check status
+ expect([200]).toContain(response.status);
+
+ // Check headers
+ expectJsonContentType(response.headers);
+
+ // Basic sanity checks on response structure
+ expect(response.data).toBeDefined();
+ expect(response.data).toHaveProperty('data');
+ expect(response.data).toHaveProperty('meta');
+ expect(Array.isArray(response.data.data)).toBe(true);
+ });
+
+ // 3. Invalid query parameter type (e.g., page is string)
+ it('should return 400 or 422 when "page" parameter is invalid type', async () => {
+ const response: AxiosResponse = await axiosInstance.get('/api/v1/schedules', {
+ params: {
+ page: 'invalidType',
+ },
+ });
+
+ // Expecting 400 or 422 for invalid parameter type
+ expect([400, 422]).toContain(response.status);
+
+ // Response headers
+ expectJsonContentType(response.headers);
+ });
+
+ // 4. Out-of-bound query parameter (e.g., negative page)
+ it('should return 400 or 422 when "page" parameter is negative', async () => {
+ const response: AxiosResponse = await axiosInstance.get('/api/v1/schedules', {
+ params: {
+ page: -1,
+ },
+ });
+
+ // Expecting 400 or 422
+ expect([400, 422]).toContain(response.status);
+
+ // Response headers
+ expectJsonContentType(response.headers);
+ });
+
+ // 5. Edge case: Large perPage value
+ it('should handle a large perPage value (e.g. 999999)', async () => {
+ const response: AxiosResponse = await axiosInstance.get('/api/v1/schedules', {
+ params: {
+ perPage: 999999,
+ },
+ });
+
+ // The API may return 200 and respond with a large dataset or an error.
+ // Typically it might still return 200, so we check for both.
+ // If the API absolutely disallows large perPage, it might return 400 or 422.
+ expect([200, 400, 422]).toContain(response.status);
+
+ if (response.status === 200) {
+ expectJsonContentType(response.headers);
+ expect(response.data).toHaveProperty('data');
+ expect(response.data).toHaveProperty('meta');
+ expect(Array.isArray(response.data.data)).toBe(true);
+ }
+ });
+
+ // 6. Authorized vs Unauthorized requests
+ it('should return 200 with valid authorization token', async () => {
+ // If authToken is valid, expect 200
+ // If not valid, test might fail or return 401/403
+ // This is just a basic check with valid token environment setup.
+ const response: AxiosResponse = await axiosInstance.get('/api/v1/schedules');
+ expect([200]).toContain(response.status);
+ expectJsonContentType(response.headers);
+ });
+
+ it('should return 401 or 403 if no authorization token is provided', async () => {
+ // Create a second instance without the Authorization header
+ const noAuthAxios = axios.create({
+ baseURL,
+ validateStatus: () => true,
+ });
+
+ const response: AxiosResponse = await noAuthAxios.get('/api/v1/schedules');
+
+ // Expect 401 or 403
+ expect([401, 403]).toContain(response.status);
+ });
+
+ // 7. Handling empty or no results found scenario
+ // This test is conceptual, as it depends on the state of the system.
+ // One approach: request a page number so large that no results should exist.
+ it('should return an empty data array if no schedules are found', async () => {
+ const response: AxiosResponse = await axiosInstance.get('/api/v1/schedules', {
+ params: {
+ page: 999999, // hoping to get an empty list
+ },
+ });
+
+ // Ensure valid 200 status
+ expect([200]).toContain(response.status);
+ expectJsonContentType(response.headers);
+
+ // Check for empty result array if it is truly empty
+ expect(response.data).toBeDefined();
+ expect(response.data).toHaveProperty('data');
+
+ if (Array.isArray(response.data.data)) {
+ // If no schedules exist on this page, we expect an empty array
+ // (Can't guarantee environment data, so we do a safe check)
+ if (response.data.data.length === 0) {
+ expect(response.data.data.length).toBe(0);
+ } else {
+ // If we do have data, at least it is a valid array
+ expect(Array.isArray(response.data.data)).toBe(true);
+ }
+ }
+ });
+
+ // 8. Server error (5xx) scenario
+ // Hard to force a 5xx in a test environment without special setup.
+ // Below is a placeholder in case you can simulate a server error.
+ // it('should handle 500 internal server error scenario', async () => {
+ // // If the server can be forced to error, test it here.
+ // // In normal situations, you might mock or intercept the request.
+ // // Or test a known edge case for the server.
+ // });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.py b/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.py
new file mode 100644
index 0000000000..e86ec9c115
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.py
@@ -0,0 +1,124 @@
+import axios, { AxiosResponse } from 'axios';
+
+describe('/api/v1/timezones GET', () => {
+ const baseUrl = process.env.API_BASE_URL;
+ const authToken = process.env.API_AUTH_TOKEN;
+
+ // Utility function to build the request config for axios
+ const getRequestConfig = (overrideToken?: string) => {
+ const token = overrideToken !== undefined ? overrideToken : authToken;
+ return {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ validateStatus: () => true, // Allow Axios to resolve promise for all status codes
+ };
+ };
+
+ // Utility to create a full URL with optional query param
+ const buildUrl = (excludeUtc?: string | boolean) => {
+ if (!baseUrl) {
+ throw new Error('API_BASE_URL is not defined');
+ }
+
+ let url = `${baseUrl}/api/v1/timezones`;
+
+ if (excludeUtc !== undefined) {
+ url += `?excludeUtc=${excludeUtc}`;
+ }
+ return url;
+ };
+
+ // 1. Input Validation
+ // Test with no query param (excludeUtc defaults to false)
+ test('should return 200 OK when called without query param', async () => {
+ const response: AxiosResponse = await axios.get(buildUrl(), getRequestConfig());
+ expect([200]).toContain(response.status);
+ });
+
+ // Test with excludeUtc = true
+ test('should return 200 OK when excludeUtc = true', async () => {
+ const response: AxiosResponse = await axios.get(buildUrl(true), getRequestConfig());
+ expect([200]).toContain(response.status);
+ });
+
+ // Test with excludeUtc = false
+ test('should return 200 OK when excludeUtc = false', async () => {
+ const response: AxiosResponse = await axios.get(buildUrl(false), getRequestConfig());
+ expect([200]).toContain(response.status);
+ });
+
+ // Test with invalid type for excludeUtc
+ test('should return 400 or 422 when excludeUtc is invalid type', async () => {
+ const response: AxiosResponse = await axios.get(buildUrl('not-a-boolean'), getRequestConfig());
+ expect([400, 422]).toContain(response.status);
+ });
+
+ // 2. Response Validation
+ // Validate the response schema and content
+ test('should return a valid JSON array of timezones for a valid request', async () => {
+ const response: AxiosResponse = await axios.get(buildUrl(), getRequestConfig());
+ // Verify 200
+ expect(response.status).toBe(200);
+ // Verify content-type
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Basic schema validation
+ // Assuming the schema has a top-level property "timezones" that is an array of strings
+ const data = response.data;
+ expect(data).toBeDefined();
+ // Example check for schema: { timezones: string[] }
+ expect(Array.isArray(data.timezones)).toBe(true);
+ // Check if each item is a string
+ data.timezones.forEach((tz: unknown) => {
+ expect(typeof tz).toBe('string');
+ });
+ });
+
+ // 3. Response Headers Validation
+ test('should include application/json in content-type header', async () => {
+ const response: AxiosResponse = await axios.get(buildUrl(), getRequestConfig());
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ // 4. Edge Case & Limit Testing
+ // Test unauthorized request (no token or invalid token)
+ test('should return 401 or 403 when called without valid auth token', async () => {
+ const invalidAuthToken = 'invalid-token';
+ const response: AxiosResponse = await axios.get(buildUrl(), getRequestConfig(invalidAuthToken));
+ expect([401, 403]).toContain(response.status);
+ });
+
+ // Test scenario with malformed query param (already covered above, but another example)
+ test('should return 400 or 422 for empty string query param', async () => {
+ const response: AxiosResponse = await axios.get(buildUrl(''), getRequestConfig());
+ // Some APIs may interpret empty query param as invalid, or default it.
+ // We'll assume invalid.
+ if (response.status !== 200) {
+ expect([400, 422]).toContain(response.status);
+ } else {
+ // If it returns 200, then the service gracefully handled the empty query.
+ expect(response.headers['content-type']).toContain('application/json');
+ }
+ });
+
+ // Optionally test behavior when no results are found (depends on service data)
+ // This may require a special server state or mocking. Here is a placeholder.
+ test('should handle empty results gracefully (placeholder test)', async () => {
+ // This test will pass if we simply call the endpoint and get a valid response.
+ // A real test might require specific server setup to get an empty array.
+ const response: AxiosResponse = await axios.get(buildUrl(), getRequestConfig());
+ expect([200]).toContain(response.status);
+ // Additional checks could be made if the server can return an empty array.
+ });
+
+ // 5. Testing Authorization & Authentication (covered partially by invalid token test)
+ // Additional test for missing token
+ test('should return 401 or 403 when called with no token at all', async () => {
+ const config = {
+ validateStatus: () => true,
+ };
+ const response: AxiosResponse = await axios.get(buildUrl(), config);
+ expect([401, 403]).toContain(response.status);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.py b/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.py
new file mode 100644
index 0000000000..c1ba74d6b4
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.py
@@ -0,0 +1 @@
+import { describe, it, expect } from '@jest/globals';\nimport axios, { AxiosResponse } from 'axios';\n\nconst BASE_URL = process.env.API_BASE_URL;\nconst AUTH_TOKEN = process.env.API_AUTH_TOKEN;\n\n/**\n * Utility function to create an Axios instance with optional authorization.\n */\nfunction createAxiosInstance(token?: string) {\n return axios.create({\n baseURL: BASE_URL,\n headers: token\n ? {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n }\n : {\n 'Content-Type': 'application/json',\n },\n });\n}\n\ndescribe('GET /api/v3/runs/{runId}', () => {\n // Replace with a real or mocked run ID known to exist in your testing environment.\n const validRunId = 'valid_run_id_123';\n\n // A run ID that does not exist in the system\n const nonExistentRunId = 'nonexistent_run_id_999';\n\n // Various invalid run IDs for input validation\n const invalidRunIds = ['', ' ', '!!!', '12345678901234567890123456789012345'];\n\n // Check environment variables\n if (!BASE_URL) {\n throw new Error('Missing API_BASE_URL environment variable');\n }\n\n //------------------------------------------------------------------------------\n // 1. Missing token (Should return 401 or 403)\n //------------------------------------------------------------------------------\n it('should return 401 or 403 for requests without an auth token', async () => {\n const instance = createAxiosInstance();\n let status: number | undefined;\n try {\n await instance.get(`/api/v3/runs/${validRunId}`);\n } catch (error: any) {\n const response: AxiosResponse = error.response;\n status = response.status;\n }\n expect([401, 403]).toContain(status);\n });\n\n //------------------------------------------------------------------------------\n // 2. Invalid token (Should return 401 or 403)\n //------------------------------------------------------------------------------\n it('should return 401 or 403 for requests with an invalid auth token', async () => {\n const instance = createAxiosInstance('invalid_token');\n let status: number | undefined;\n try {\n await instance.get(`/api/v3/runs/${validRunId}`);\n } catch (error: any) {\n const response: AxiosResponse = error.response;\n status = response.status;\n }\n expect([401, 403]).toContain(status);\n });\n\n //------------------------------------------------------------------------------\n // 3. Valid token, but run not found (Should return 404)\n //------------------------------------------------------------------------------\n it('should return 404 if the run is not found', async () => {\n if (!AUTH_TOKEN) {\n console.warn('Skipping test due to missing API_AUTH_TOKEN');\n return;\n }\n const instance = createAxiosInstance(AUTH_TOKEN);\n let status: number | undefined;\n try {\n await instance.get(`/api/v3/runs/${nonExistentRunId}`);\n } catch (error: any) {\n const response: AxiosResponse = error.response;\n status = response.status;\n }\n expect(status).toBe(404);\n });\n\n //------------------------------------------------------------------------------\n // 4. Invalid runId input (Should return 400 or 422)\n //------------------------------------------------------------------------------\n invalidRunIds.forEach((id) => {\n it(`should return 400 or 422 when runId is '${id}'`, async () => {\n if (!AUTH_TOKEN) {\n console.warn('Skipping test due to missing API_AUTH_TOKEN');\n return;\n }\n const instance = createAxiosInstance(AUTH_TOKEN);\n let status: number | undefined;\n try {\n await instance.get(`/api/v3/runs/${id}`);\n } catch (error: any) {\n const response: AxiosResponse = error.response;\n status = response.status;\n }\n expect([400, 422]).toContain(status);\n });\n });\n\n //------------------------------------------------------------------------------\n // 5. Valid token, valid runId, successful retrieval (Should return 200)\n //------------------------------------------------------------------------------\n it('should return 200 and a valid run object for a valid runId with a valid token', async () => {\n if (!AUTH_TOKEN) {\n console.warn('Skipping test due to missing API_AUTH_TOKEN');\n return;\n }\n const instance = createAxiosInstance(AUTH_TOKEN);\n const response = await instance.get(`/api/v3/runs/${validRunId}`);\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n\n // Basic schema validation\n const data = response.data;\n // Adjust checks according to #/components/schemas/RetrieveRunResponse\n expect(typeof data).toBe('object');\n expect(typeof data.runId).toBe('string');\n expect(typeof data.status).toBe('string');\n // payload and output might be omitted if using a public key, so check if they exist\n // without failing the test if undefined.\n if (data.payload !== undefined) {\n expect(typeof data.payload).toBe('object');\n }\n if (data.output !== undefined) {\n expect(typeof data.output).toBe('object');\n }\n // attempts could be an array, number, or some structure depending on the schema\n // For demonstration, we assume it's an array.\n expect(Array.isArray(data.attempts)).toBe(true);\n });\n\n //------------------------------------------------------------------------------\n // 6. Check response headers besides Content-Type (optional)\n //------------------------------------------------------------------------------\n it('should include standard headers (e.g., Cache-Control, X-RateLimit*) if applicable', async () => {\n if (!AUTH_TOKEN) {\n console.warn('Skipping test due to missing API_AUTH_TOKEN');\n return;\n }\n const instance = createAxiosInstance(AUTH_TOKEN);\n const response = await instance.get(`/api/v3/runs/${validRunId}`);\n\n // Example checks\n expect(response.headers['content-type']).toContain('application/json');\n // Optional checks for X-RateLimit\n // expect(response.headers['x-ratelimit-limit']).toBeDefined();\n // expect(response.headers['x-ratelimit-remaining']).toBeDefined();\n // Expect no caching or a specified caching policy\n // expect(response.headers['cache-control']).toBeDefined();\n });\n\n //------------------------------------------------------------------------------\n // 7. Edge and limit testing: extremely large runId\n //------------------------------------------------------------------------------\n it('should handle extremely large runId gracefully', async () => {\n if (!AUTH_TOKEN) {\n console.warn('Skipping test due to missing API_AUTH_TOKEN');\n return;\n }\n const instance = createAxiosInstance(AUTH_TOKEN);\n const largeRunId = '9'.repeat(1000); // Very large numeric string\n\n let status: number | undefined;\n try {\n await instance.get(`/api/v3/runs/${largeRunId}`);\n } catch (error: any) {\n const response: AxiosResponse = error.response;\n status = response.status;\n }\n // Depending on implementation, might be 400, 422, or 404\n expect([400, 422, 404]).toContain(status);\n });\n\n //------------------------------------------------------------------------------\n // You could add more tests for rate-limiting, server errors, forbidden, etc.\n //------------------------------------------------------------------------------\n});\n
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.py
new file mode 100644
index 0000000000..3f7bbb8c95
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.py
@@ -0,0 +1,204 @@
+import axios, { AxiosInstance, AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
+
+// Ensure you have the following environment variables set:
+// - API_BASE_URL (e.g., https://your-api.example.com)
+// - API_AUTH_TOKEN (e.g., an authentication token)
+
+// This test suite covers the POST /api/v1/projects/:projectRef/envvars/:env/import endpoint.
+// It validates request input, response structure, status codes, headers, and edge-case scenarios.
+
+const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+const authToken = process.env.API_AUTH_TOKEN || 'YOUR_AUTH_TOKEN';
+
+// For demonstration, we use sample values for the path parameters.
+// Adjust these to match your testing environment.
+const TEST_PROJECT_REF = 'sample-project';
+const TEST_ENV = 'development';
+
+// Helper method to create an Axios instance.
+function createAxiosInstance(token?: string): AxiosInstance {
+ return axios.create({
+ baseURL,
+ headers: token
+ ? {
+ Authorization: `Bearer ${token}`,
+ }
+ : {},
+ validateStatus: () => true, // Let us handle status checks manually.
+ });
+}
+
+// Valid payload structure assumed based on typical environment variable import:
+// Adjust fields/properties as required by your actual API schema.
+interface EnvVar {
+ key: string;
+ value: string;
+}
+
+interface ImportEnvVarsRequest {
+ variables: EnvVar[];
+}
+
+// A sample valid payload.
+const validPayload: ImportEnvVarsRequest = {
+ variables: [
+ { key: 'API_KEY', value: 'myApiKey123' },
+ { key: 'SECRET', value: 'superSecretValue' },
+ ],
+};
+
+// A sample invalid payload (missing "key" or "value" fields, or using the wrong types).
+const invalidPayload: any = {
+ variables: [
+ { wrongKeyName: 'NO_KEY', wrongValueName: 123 },
+ ],
+};
+
+describe('POST /api/v1/projects/:projectRef/envvars/:env/import', () => {
+ let axiosInstance: AxiosInstance;
+
+ beforeAll(() => {
+ // Create an Axios instance with a valid auth token (if required by the API).
+ axiosInstance = createAxiosInstance(authToken);
+ });
+
+ afterAll(() => {
+ // Clean up or reset if necessary.
+ });
+
+ it('should successfully upload environment variables with a valid payload (200)', async () => {
+ const url = `/api/v1/projects/${TEST_PROJECT_REF}/envvars/${TEST_ENV}/import`;
+
+ const response: AxiosResponse = await axiosInstance.post(url, validPayload);
+
+ // Check the expected status code.
+ expect(response.status).toBe(200);
+
+ // Check response headers.
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Validate response body structure (assuming a success schema with a "message" or similar).
+ // Replace with actual schema validations if needed.
+ expect(response.data).toBeDefined();
+ // For example, if #/components/schemas/SucceedResponse has a "success" field:
+ // expect(response.data.success).toBe(true);
+ });
+
+ it('should return 400 or 422 for invalid payload', async () => {
+ const url = `/api/v1/projects/${TEST_PROJECT_REF}/envvars/${TEST_ENV}/import`;
+
+ const response: AxiosResponse = await axiosInstance.post(url, invalidPayload);
+
+ // The API might return 400 or 422 for invalid requests.
+ expect([400, 422]).toContain(response.status);
+
+ // Check response headers.
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Optional: Validate error response schema (assuming an error property exists).
+ // e.g., expect(response.data).toHaveProperty('error');
+ });
+
+ it('should fail with 401 or 403 if token is missing/invalid', async () => {
+ // Create an axios instance with no token (or an invalid token).
+ const unauthorizedAxios = createAxiosInstance('INVALID_TOKEN');
+
+ const url = `/api/v1/projects/${TEST_PROJECT_REF}/envvars/${TEST_ENV}/import`;
+
+ const response: AxiosResponse = await unauthorizedAxios.post(url, validPayload);
+
+ // The API might return 401 (Unauthorized) or 403 (Forbidden).
+ expect([401, 403]).toContain(response.status);
+
+ // Check response headers.
+ // Some APIs may return text/html for error pages, so adjust accordingly.
+ if (response.headers['content-type']) {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ }
+
+ // Optionally validate error payload, if applicable.
+ });
+
+ it('should return 404 if the projectRef or env does not exist', async () => {
+ // Use invalid projectRef/env to trigger 404.
+ const invalidProjectRef = 'nonexistent-project';
+ const invalidEnv = 'unknown-env';
+ const url = `/api/v1/projects/${invalidProjectRef}/envvars/${invalidEnv}/import`;
+
+ const response: AxiosResponse = await axiosInstance.post(url, validPayload);
+
+ // Check that not found is returned.
+ expect(response.status).toBe(404);
+
+ // Check response headers.
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ it('should handle large payload without error (if supported by API)', async () => {
+ // Construct a large array of environment variables.
+ const largeVariables: EnvVar[] = [];
+ for (let i = 0; i < 1000; i++) {
+ largeVariables.push({ key: `KEY_${i}`, value: `VALUE_${i}` });
+ }
+
+ const largePayload: ImportEnvVarsRequest = {
+ variables: largeVariables,
+ };
+
+ const url = `/api/v1/projects/${TEST_PROJECT_REF}/envvars/${TEST_ENV}/import`;
+
+ const response: AxiosResponse = await axiosInstance.post(url, largePayload);
+
+ // Depending on the API, it should return 200 if it can handle large payloads.
+ // Adjust if your API returns a 413 Payload Too Large or another status.
+ expect([200, 413]).toContain(response.status);
+
+ if (response.status === 200) {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ // Validate success response...
+ }
+ });
+
+ it('should handle boundary values (e.g., empty strings, special chars)', async () => {
+ const boundaryPayload: ImportEnvVarsRequest = {
+ variables: [
+ { key: '', value: '' }, // empty
+ { key: 'SPECIAL_CHARS', value: '!@#$%^&*()_+' },
+ ],
+ };
+
+ const url = `/api/v1/projects/${TEST_PROJECT_REF}/envvars/${TEST_ENV}/import`;
+
+ const response: AxiosResponse = await axiosInstance.post(url, boundaryPayload);
+
+ // Expect success or validation error, depending on API rules.
+ expect([200, 400, 422]).toContain(response.status);
+
+ if (response.status === 200) {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ // e.g., expect(response.data.success).toBe(true);
+ } else {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ // e.g., expect(response.data).toHaveProperty('error');
+ }
+ });
+
+ it('should handle a server error (500) gracefully (if applicable)', async () => {
+ // Triggering a 500 typically requires forcing the server to error.
+ // This test is often environment-specific. We'll do a mock scenario:
+ // For example, sending malicious data or something that you know triggers an error.
+
+ const errorInducingPayload: ImportEnvVarsRequest = {
+ variables: [{ key: 'FORCE_ERROR', value: 'trigger-500' }],
+ };
+
+ const url = `/api/v1/projects/${TEST_PROJECT_REF}/envvars/${TEST_ENV}/import`;
+
+ const response: AxiosResponse = await axiosInstance.post(url, errorInducingPayload);
+
+ // Expect 500 or another custom code if the server encountered an error.
+ // If your API doesn’t actually throw 500, you can skip or adjust this test.
+ expect([200, 500]).toContain(response.status);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.py
new file mode 100644
index 0000000000..a3fc22974a
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.py
@@ -0,0 +1 @@
+import axios, { AxiosResponse, AxiosError } from 'axios';\nimport { describe, it, expect } from '@jest/globals';\n\nconst baseURL = process.env.API_BASE_URL;\nconst authToken = process.env.API_AUTH_TOKEN;\n\nfunction getEndpoint(projectRef: string, env: string) {\n return `${baseURL}/api/v1/projects/${projectRef}/envvars/${env}`;\n}\n\n// Example of a valid request body for creating an environment variable\nconst validEnvVarBody = {\n key: 'TEST_KEY',\n value: 'TEST_VALUE',\n};\n\ndescribe('POST /api/v1/projects/{projectRef}/envvars/{env}', () => {\n /*\n 1. Valid Request Test (200)\n Checks that a correct payload, valid path parameters, and valid auth token\n produce a 200 response and conform to the success schema.\n */\n it('should create environment variable with valid input (200)', async () => {\n const projectRef = '123';\n const env = 'dev';\n\n try {\n const response: AxiosResponse = await axios.post(\n getEndpoint(projectRef, env),\n validEnvVarBody,\n {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json',\n },\n }\n );\n\n expect(response.status).toBe(200);\n // Verify response body has a "message" or similar success property\n expect(response.data).toHaveProperty('message');\n // Verify the Content-Type header\n expect(response.headers['content-type']).toMatch(/application\\/json/i);\n } catch (error: any) {\n throw new Error(`Expected 200 but received ${error?.response?.status}. Error: ${error.message}`);\n }\n });\n\n /*\n 2. Input Validation Test (400 or 422)\n Attempts to create the environment variable with invalid body parameters.\n We expect a 400 or 422 error.\n */\n it('should return 400 or 422 for invalid body', async () => {\n const projectRef = '123';\n const env = 'dev';\n\n // Missing the "value" field\n const invalidBody = {\n key: 'TEST_KEY',\n // value: 'TEST_VALUE'\n };\n\n try {\n await axios.post(getEndpoint(projectRef, env), invalidBody, {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json',\n },\n });\n throw new Error('Expected request to fail with 400 or 422');\n } catch (error: any) {\n // Some APIs may return 422 instead of 400 for malformed data.\n expect([400, 422]).toContain(error?.response?.status);\n expect(error.response.data).toHaveProperty('error');\n expect(error.response.headers['content-type']).toMatch(/application\\/json/i);\n }\n });\n\n /*\n 3. Authorization/Authentication Test (401 or 403)\n Ensures that requests without valid credentials are rejected.\n */\n it('should return 401 or 403 for missing or invalid auth token', async () => {\n const projectRef = '123';\n const env = 'dev';\n\n try {\n await axios.post(\n getEndpoint(projectRef, env),\n validEnvVarBody,\n {\n headers: {\n // Intentionally leaving out Authorization header\n 'Content-Type': 'application/json',\n },\n }\n );\n throw new Error('Expected request to fail with 401 or 403');\n } catch (error: any) {\n // API might return 401 Unauthorized or 403 Forbidden\n expect([401, 403]).toContain(error?.response?.status);\n expect(error.response.data).toHaveProperty('error');\n expect(error.response.headers['content-type']).toMatch(/application\\/json/i);\n }\n });\n\n /*\n 4. Resource Not Found Test (404)\n Attempts to create an environment variable for a non-existent projectRef or env.\n */\n it('should return 404 if projectRef or env does not exist', async () => {\n const nonExistentProjectRef = 'nonExistentProject';\n const nonExistentEnv = 'nonExistentEnv';\n\n try {\n await axios.post(\n getEndpoint(nonExistentProjectRef, nonExistentEnv),\n validEnvVarBody,\n {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json',\n },\n }\n );\n throw new Error('Expected request to fail with 404');\n } catch (error: any) {\n expect(error?.response?.status).toBe(404);\n expect(error.response.data).toHaveProperty('error');\n expect(error.response.headers['content-type']).toMatch(/application\\/json/i);\n }\n });\n\n /*\n 5. Edge Case & Limit Testing\n Tests creating very large environment variable values.\n The API may accept or reject these requests depending on size limits.\n */\n it('should handle large payload gracefully', async () => {\n const projectRef = '123';\n const env = 'dev';\n\n const largeBody = {\n key: 'TEST_KEY_LARGE',\n // Create a very large string for the "value"\n value: 'A'.repeat(10000),\n };\n\n try {\n const response: AxiosResponse = await axios.post(\n getEndpoint(projectRef, env),\n largeBody,\n {\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json',\n },\n }\n );\n\n // If the server permits large payloads, expect a 200\n expect(response.status).toBe(200);\n expect(response.data).toHaveProperty('message');\n expect(response.headers['content-type']).toMatch(/application\\/json/i);\n } catch (error: any) {\n // If the server rejects large payloads, we might see 400 or 413 (Payload Too Large)\n expect([400, 413]).toContain(error?.response?.status);\n expect(error.response.data).toHaveProperty('error');\n expect(error.response.headers['content-type']).toMatch(/application\\/json/i);\n }\n });\n});\n
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.py
new file mode 100644
index 0000000000..6c9b905c48
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.py
@@ -0,0 +1 @@
+import axios from 'axios';\nimport { AxiosResponse, AxiosError } from 'axios';\n\n/**\n * Jest test suite for POST /api/v1/runs/{runId}/replay\n *\n * This test suite verifies the following:\n * 1. Input Validation (runId and any other parameters)\n * 2. Response Validation (status codes and body schema)\n * 3. Response Headers Validation (content-type, etc.)\n * 4. Edge Case & Limit Testing (large or malformed inputs, not found, etc.)\n * 5. Authorization & Authentication Tests (valid and invalid tokens)\n */\n\n// Load environment variables\nconst baseURL = process.env.API_BASE_URL || 'http://localhost:3000';\nconst validToken = process.env.API_AUTH_TOKEN || 'VALID_TOKEN_EXAMPLE';\n\n// Utility function to help with making requests\n// (allows us to reuse logic for different tests).\nconst replayRun = async (\n runId: string,\n token: string | null = validToken\n): Promise> => {\n const headers: Record = {\n 'Content-Type': 'application/json'\n };\n if (token) {\n headers['Authorization'] = 'Bearer ' + token;\n }\n\n return axios.post(`${baseURL}/api/v1/runs/${runId}/replay`, null, { headers });\n};\n\ndescribe('POST /api/v1/runs/{runId}/replay', () => {\n /**\n * 1. Valid request test\n */\n test('Should replay run successfully with a valid runId', async () => {\n // Replace with a known valid run ID if your environment requires\n const runId = 'validRunId';\n\n const response = await replayRun(runId, validToken);\n expect(response.status).toBe(200);\n\n // Response body validation\n expect(response.data).toHaveProperty('id');\n expect(typeof response.data.id).toBe('string');\n\n // Headers validation\n expect(response.headers).toHaveProperty('content-type');\n expect(response.headers['content-type']).toContain('application/json');\n });\n\n /**\n * 2. Input Validation -- invalid or missing runId\n * Expect a 400/422 (bad request) or 404 (not found) depending on API implementation\n */\n test('Should return 400 or 422 if runId is empty', async () => {\n const emptyRunId = '';\n try {\n await replayRun(emptyRunId);\n // If we reach here, the request did not fail as expected\n fail('Request should not succeed with empty runId');\n } catch (err) {\n const axiosError = err as AxiosError;\n expect([400, 422, 404]).toContain(axiosError.response?.status);\n // Optionally check error response body\n expect(axiosError.response?.data).toHaveProperty('error');\n }\n });\n\n test('Should return 400 or 422 if runId is invalid (malformed characters)', async () => {\n const invalidRunId = '!@#$%^&*()';\n try {\n await replayRun(invalidRunId);\n fail('Request should not succeed with malformed runId');\n } catch (err) {\n const axiosError = err as AxiosError;\n expect([400, 422, 404]).toContain(axiosError.response?.status);\n expect(axiosError.response?.data).toHaveProperty('error');\n }\n });\n\n /**\n * 3. Not Found scenarios (404)\n */\n test('Should return 404 if runId does not exist', async () => {\n // Replace with an ID that you are sure does not exist\n const nonExistentRunId = 'nonExistentId';\n try {\n await replayRun(nonExistentRunId);\n fail('Request should not succeed with non-existent runId');\n } catch (err) {\n const axiosError = err as AxiosError;\n // 404 is the typical not found response\n expect(axiosError.response?.status).toBe(404);\n expect(axiosError.response?.data).toHaveProperty('error');\n expect(axiosError.response?.data.error).toMatch(/Run not found/);\n }\n });\n\n /**\n * 4. Authorization & Authentication tests\n * Expect 401 or 403 if token is missing or invalid.\n */\n test('Should return 401 or 403 if Authorization header is missing', async () => {\n const runId = 'validRunId';\n try {\n await replayRun(runId, null);\n fail('Request should not succeed without authorization token');\n } catch (err) {\n const axiosError = err as AxiosError;\n expect([401, 403]).toContain(axiosError.response?.status);\n expect(axiosError.response?.data).toHaveProperty('error');\n expect(axiosError.response?.data.error).toMatch(/Invalid or Missing API key/i);\n }\n });\n\n test('Should return 401 or 403 if Authorization token is invalid', async () => {\n const runId = 'validRunId';\n const invalidToken = 'Invalid_Token';\n try {\n await replayRun(runId, invalidToken);\n fail('Request should not succeed with invalid token');\n } catch (err) {\n const axiosError = err as AxiosError;\n expect([401, 403]).toContain(axiosError.response?.status);\n expect(axiosError.response?.data).toHaveProperty('error');\n expect(axiosError.response?.data.error).toMatch(/Invalid or Missing API key/i);\n }\n });\n\n /**\n * 5. Edge Case & Limit Testing\n */\n test('Should handle extremely large runId values gracefully (400, 422, or 404)', async () => {\n // A large runId that might exceed typical length limits\n const largeRunId = 'a'.repeat(1000);\n try {\n await replayRun(largeRunId);\n fail('Request should not succeed with extremely large runId');\n } catch (err) {\n const axiosError = err as AxiosError;\n expect([400, 422, 404]).toContain(axiosError.response?.status);\n expect(axiosError.response?.data).toHaveProperty('error');\n }\n });\n\n test('Should gracefully handle server errors (5xx)', async () => {\n // This test is hypothetical if the server returns a 500 for some internal error scenario\n // We cannot guarantee 500 from a real server unless we cause it deliberately.\n // However, having a test ensures we handle it gracefully.\n const runId = 'triggerInternalServerError';\n try {\n await replayRun(runId);\n fail('Request should fail with a 5xx error');\n } catch (err) {\n const axiosError = err as AxiosError;\n if (axiosError.response) {\n // 500 or other 5xx status codes\n if (axiosError.response.status >= 500 && axiosError.response.status < 600) {\n expect(axiosError.response.data).toHaveProperty('error');\n } else {\n // It might return something else if the server doesn't truly throw a 500\n expect([400, 404]).toContain(axiosError.response.status);\n }\n }\n }\n });\n});\n
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.py
new file mode 100644
index 0000000000..3d28c73a0f
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.py
@@ -0,0 +1,126 @@
+import axios from 'axios';
+import { AxiosInstance } from 'axios';
+
+describe('POST /api/v1/runs/{runId}/reschedule', () => {
+ let client: AxiosInstance;
+ const baseUrl = process.env.API_BASE_URL;
+ const authToken = process.env.API_AUTH_TOKEN;
+
+ beforeAll(() => {
+ client = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+ });
+
+ describe('Input Validation tests', () => {
+ it('should return 400 or 422 if runId is invalid', async () => {
+ try {
+ const invalidRunId = 'abc';
+ await client.post(`/api/v1/runs/${invalidRunId}/reschedule`, {
+ // Simulate a valid payload otherwise
+ delay: 30,
+ });
+ fail(`Request should have failed with 400 or 422`);
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ expect(error.response.data).toHaveProperty('error');
+ }
+ });
+
+ it('should return 400 or 422 when required fields are missing', async () => {
+ try {
+ const runId = 123;
+ // Missing "delay" field
+ await client.post(`/api/v1/runs/${runId}/reschedule`, {});
+ fail(`Request should have failed with 400 or 422`);
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ expect(error.response.data).toHaveProperty('error');
+ }
+ });
+ });
+
+ describe('Successful Response tests', () => {
+ it('should reschedule a delayed run with valid runId and payload', async () => {
+ // Assuming runId 999 is in DELAYED state on the test system
+ const runId = 999;
+ const response = await client.post(`/api/v1/runs/${runId}/reschedule`, {
+ delay: 60,
+ });
+
+ // Response Validation
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('status');
+ });
+ });
+
+ describe('Authorization & Authentication tests', () => {
+ it('should return 401 or 403 if auth token is missing or invalid', async () => {
+ const runId = 123;
+ const unauthorizedClient = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ 'Content-Type': 'application/json',
+ // No Authorization header
+ },
+ });
+
+ try {
+ await unauthorizedClient.post(`/api/v1/runs/${runId}/reschedule`, {
+ delay: 50,
+ });
+ fail(`Request should have failed with 401 or 403`);
+ } catch (error: any) {
+ expect([401, 403]).toContain(error.response.status);
+ expect(error.response.data).toHaveProperty('error');
+ }
+ });
+ });
+
+ describe('Resource not found tests', () => {
+ it('should return 404 if the run does not exist', async () => {
+ try {
+ const nonExistentId = 9999999;
+ await client.post(`/api/v1/runs/${nonExistentId}/reschedule`, {
+ delay: 10,
+ });
+ fail(`Request should have failed with 404`);
+ } catch (error: any) {
+ expect(error.response.status).toBe(404);
+ expect(error.response.data).toHaveProperty('error');
+ }
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ it('should handle large delay values', async () => {
+ // Test a large numerical boundary for delay
+ const runId = 999; // Assume DELAYED state
+ const largeDelay = 9999999;
+ const response = await client.post(`/api/v1/runs/${runId}/reschedule`, {
+ delay: largeDelay,
+ });
+ // Depending on API handling, might be success or error
+ expect([200, 400, 422]).toContain(response.status);
+ });
+
+ it('should return an error if the run is not in DELAYED state', async () => {
+ // This runId is assumed not to be in DELAYED state
+ const runId = 555;
+ try {
+ await client.post(`/api/v1/runs/${runId}/reschedule`, {
+ delay: 20,
+ });
+ fail(`Request should have failed due to the run not being in DELAYED state`);
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.py
new file mode 100644
index 0000000000..df431367a6
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.py
@@ -0,0 +1,130 @@
+import axios, { AxiosInstance } from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+// This interface can be updated to match the actual ScheduleObject schema
+interface ScheduleObject {
+ id: string;
+ name?: string;
+ status?: string;
+ // Add any other fields expected in the response
+}
+
+// Create an Axios instance with default configuration
+const apiClient: AxiosInstance = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`,
+ },
+});
+
+describe('/api/v1/schedules/{schedule_id}/activate (POST) - Activate Schedule', () => {
+ // 1) Input Validation - Required parameter test
+ test('Should return 400 or 422 when schedule_id is missing or invalid', async () => {
+ // Example of an invalid schedule id
+ const invalidScheduleIds = ['', ' ', '!!!', '12345'];
+
+ for (const scheduleId of invalidScheduleIds) {
+ try {
+ await apiClient.post(`/api/v1/schedules/${scheduleId}/activate`);
+ // If the request does not throw, the test should fail
+ fail(
+ `Expected an error for invalid schedule_id "${scheduleId}", but request succeeded.`
+ );
+ } catch (error: any) {
+ // Check if the response is 400 or 422, which indicates invalid input
+ const statusCode = error?.response?.status;
+ expect([400, 422]).toContain(statusCode);
+ }
+ }
+ });
+
+ // 2) Response Validation - Successful activation
+ // NOTE: Update "validScheduleId" with a valid IMPERATIVE schedule ID in your test environment.
+ test('Should successfully activate a valid schedule (200)', async () => {
+ const validScheduleId = 'test-schedule-imperative-001'; // Replace with a real existing schedule ID
+
+ // Attempt to activate a valid schedule
+ const response = await apiClient.post(`/api/v1/schedules/${validScheduleId}/activate`);
+
+ // Check response status
+ expect(response.status).toBe(200);
+
+ // 3) Response Headers Validation
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+
+ // 4) Check the response body shape matches an expected schema
+ const responseData = response.data as ScheduleObject;
+ expect(responseData).toHaveProperty('id');
+ // Additional checks can be made by comparing to the full ScheduleObject schema
+ });
+
+ // 5) Edge Case: Non-existing schedule ID should return 404
+ // NOTE: Update "nonExistingScheduleId" if needed or keep it random
+ test('Should return 404 for non-existing schedule_id', async () => {
+ const nonExistingScheduleId = 'non-existing-schedule-id-123';
+ try {
+ await apiClient.post(`/api/v1/schedules/${nonExistingScheduleId}/activate`);
+ fail('Expected 404 for non-existing schedule_id, but request succeeded.');
+ } catch (error: any) {
+ expect(error?.response?.status).toBe(404);
+ }
+ });
+
+ // 6) Authorization & Authentication - Missing or invalid token
+ test('Should return 401 or 403 when token is invalid or missing', async () => {
+ // Create a client without authorization header
+ const unauthorizedClient = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const scheduleId = 'test-schedule-imperative-001'; // Replace with a real existing schedule
+
+ try {
+ await unauthorizedClient.post(`/api/v1/schedules/${scheduleId}/activate`);
+ fail('Expected 401 or 403 for unauthorized request, but request succeeded.');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error?.response?.status);
+ }
+ });
+
+ // 7) Edge Case: Large or special schedule_id
+ test('Should handle requests with unexpectedly large schedule_id values', async () => {
+ // Arbitrarily large string
+ const largeScheduleId = 'a'.repeat(1000);
+
+ try {
+ await apiClient.post(`/api/v1/schedules/${largeScheduleId}/activate`);
+ fail('Expected failure for excessively large schedule_id, but request succeeded.');
+ } catch (error: any) {
+ // Should respond with 400, 422, or possibly 404 if not found
+ const statusCode = error?.response?.status;
+ expect([400, 422, 404]).toContain(statusCode);
+ }
+ });
+
+ // 8) Server Error Simulation (500)
+ // NOTE: Typically, you cannot force a 500 unless the server is misconfigured or there's a mock.
+ // This is a placeholder test to illustrate how you might test for server errors.
+ test('Should handle 500 Internal Server Error gracefully (if the server responds with such)', async () => {
+ // The actual approach to trigger a 500 depends on the API. This is a purely illustrative test.
+ const scheduleId = 'trigger-500-error';
+
+ try {
+ await apiClient.post(`/api/v1/schedules/${scheduleId}/activate`);
+ // If no error is thrown, the API did not produce a 500.
+ } catch (error: any) {
+ if (error?.response?.status === 500) {
+ expect(error?.response?.status).toBe(500);
+ } else {
+ // If the server does not respond with 500, skip the assertion or check other statuses.
+ expect([400, 401, 403, 404, 422, 500]).toContain(error?.response?.status);
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.py
new file mode 100644
index 0000000000..91bbc230bc
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.py
@@ -0,0 +1,191 @@
+import axios, { AxiosError, AxiosResponse } from 'axios';
+import { config } from 'dotenv';
+
+// Load environment variables (API_BASE_URL, API_AUTH_TOKEN)
+config();
+
+// Utility function to build auth headers
+function getAuthHeaders(token?: string) {
+ return {
+ Authorization: `Bearer ${token || ''}`,
+ 'Content-Type': 'application/json',
+ };
+}
+
+/***********************************************************
+ * Jest test suite for:
+ * POST /api/v1/schedules/{schedule_id}/deactivate
+ *
+ * Summary:
+ * Deactivate a schedule by its ID (IMPERATIVE schedules only).
+ *
+ * This suite tests:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Authorization & Authentication
+ ***********************************************************/
+describe('/api/v1/schedules/{schedule_id}/deactivate', () => {
+ const API_BASE_URL = process.env.API_BASE_URL;
+ const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN;
+
+ // Replace these sample IDs with actual IDs in a real test environment
+ const validImperativeScheduleId = 'my-imperative-schedule-id';
+ const invalidFormatScheduleId = '!!!';
+ const nonexistentScheduleId = 'nonexistent-schedule-id-123';
+ // Large string for testing boundary or edge-case length
+ const extremelyLargeScheduleId = ''.padStart(10000, 'x');
+
+ // Ensure environment variables are set
+ if (!API_BASE_URL) {
+ throw new Error('Missing API_BASE_URL in environment variables');
+ }
+
+ // Helper function to build the full endpoint
+ const buildEndpoint = (scheduleId: string) => {
+ return `${API_BASE_URL}/api/v1/schedules/${scheduleId}/deactivate`;
+ };
+
+ /***********************************************************
+ * 1. INPUT VALIDATION
+ ***********************************************************/
+ describe('Input Validation', () => {
+ it('should fail with 400 or 422 if schedule_id is empty string', async () => {
+ expect.assertions(1);
+ try {
+ await axios.post(`${API_BASE_URL}/api/v1/schedules//deactivate`, {}, {
+ headers: getAuthHeaders(API_AUTH_TOKEN),
+ });
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([400, 422]).toContain(axiosError.response?.status);
+ }
+ });
+
+ it('should fail with 400 or 422 if schedule_id has invalid format', async () => {
+ expect.assertions(1);
+ try {
+ await axios.post(buildEndpoint(invalidFormatScheduleId), {}, {
+ headers: getAuthHeaders(API_AUTH_TOKEN),
+ });
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([400, 422]).toContain(axiosError.response?.status);
+ }
+ });
+ });
+
+ /***********************************************************
+ * 2. RESPONSE VALIDATION - VALID USE CASE
+ ***********************************************************/
+ describe('Response Validation (Valid Schedule)', () => {
+ it('should return 200 and a valid schedule object for a valid IMPERATIVE schedule_id', async () => {
+ // NOTE: This test expects the schedule_id to exist and be IMPERATIVE.
+ // In a real environment, set validImperativeScheduleId accordingly.
+ const response: AxiosResponse = await axios.post(
+ buildEndpoint(validImperativeScheduleId),
+ {},
+ {
+ headers: getAuthHeaders(API_AUTH_TOKEN),
+ }
+ );
+ // Check for status code
+ expect(response.status).toBe(200);
+
+ // Basic schema validation (ScheduleObject)
+ // In reality, you might check all required fields from the OpenAPI schema.
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('type');
+ expect(response.data).toHaveProperty('status');
+
+ // Additional checks can go here
+ });
+ });
+
+ /***********************************************************
+ * 3. RESPONSE HEADERS VALIDATION
+ ***********************************************************/
+ describe('Response Headers Validation', () => {
+ it('should include correct headers in the successful response', async () => {
+ // We assume a valid schedule for demonstration.
+ const response = await axios.post(
+ buildEndpoint(validImperativeScheduleId),
+ {},
+ {
+ headers: getAuthHeaders(API_AUTH_TOKEN),
+ }
+ );
+
+ // Check the status
+ expect(response.status).toBe(200);
+
+ // Check Content-Type
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Optionally check other headers like cache-control, etc.
+ // For example:
+ // expect(response.headers).toHaveProperty('cache-control');
+ });
+ });
+
+ /***********************************************************
+ * 4. EDGE CASE & LIMIT TESTING
+ ***********************************************************/
+ describe('Edge Case & Limit Testing', () => {
+ it('should return 404 if schedule_id is not found', async () => {
+ expect.assertions(1);
+ try {
+ await axios.post(buildEndpoint(nonexistentScheduleId), {}, {
+ headers: getAuthHeaders(API_AUTH_TOKEN),
+ });
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // 404 indicates resource not found
+ expect(axiosError.response?.status).toBe(404);
+ }
+ });
+
+ it('should handle extremely large schedule_id input gracefully (400 or 422)', async () => {
+ expect.assertions(1);
+ try {
+ await axios.post(buildEndpoint(extremelyLargeScheduleId), {}, {
+ headers: getAuthHeaders(API_AUTH_TOKEN),
+ });
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // Expecting 400 or 422 as invalid or unprocessable ID
+ expect([400, 422]).toContain(axiosError.response?.status);
+ }
+ });
+ });
+
+ /***********************************************************
+ * 5. TESTING AUTHORIZATION & AUTHENTICATION
+ ***********************************************************/
+ describe('Authorization & Authentication', () => {
+ it('should fail with 401 or 403 if no auth token is provided', async () => {
+ expect.assertions(1);
+ try {
+ await axios.post(buildEndpoint(validImperativeScheduleId), {}, {
+ headers: getAuthHeaders(), // No token
+ });
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError.response?.status);
+ }
+ });
+
+ it('should fail with 401 or 403 if an invalid auth token is provided', async () => {
+ expect.assertions(1);
+ try {
+ await axios.post(buildEndpoint(validImperativeScheduleId), {}, {
+ headers: getAuthHeaders('invalid-token'),
+ });
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError.response?.status);
+ }
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.py
new file mode 100644
index 0000000000..04ca5b3eef
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.py
@@ -0,0 +1,137 @@
+import axios, { AxiosInstance, AxiosError } from "axios";
+
+describe("POST /api/v1/schedules", () => {
+ let api: AxiosInstance;
+ const validPayload = {
+ name: "Test Schedule",
+ type: "IMPERATIVE",
+ startAt: "2023-10-10T10:00:00Z",
+ endAt: "2023-10-10T12:00:00Z"
+ };
+
+ beforeAll(() => {
+ const baseURL = process.env.API_BASE_URL || "http://localhost:3000";
+ const token = process.env.API_AUTH_TOKEN || "";
+
+ api = axios.create({
+ baseURL,
+ headers: {
+ Authorization: "Bearer " + token
+ }
+ });
+ });
+
+ test("should create a schedule with valid payload (200)", async () => {
+ const response = await api.post("/api/v1/schedules", validPayload);
+ expect(response.status).toBe(200);
+ // Check headers
+ expect(response.headers["content-type"]).toContain("application/json");
+ // Check response body
+ expect(response.data).toMatchObject({
+ name: validPayload.name,
+ type: validPayload.type,
+ startAt: validPayload.startAt,
+ endAt: validPayload.endAt
+ });
+ expect(response.data).toHaveProperty("id");
+ expect(response.data).toHaveProperty("createdAt");
+ expect(response.data).toHaveProperty("updatedAt");
+ });
+
+ test("should return 400 or 422 when required fields are missing", async () => {
+ const invalidPayload = {};
+ try {
+ await api.post("/api/v1/schedules", invalidPayload);
+ fail("Expected to throw an error for missing required fields");
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ test("should return 400 or 422 when fields have invalid data types", async () => {
+ const invalidPayload = {
+ name: 1234, // Should be string
+ type: "IMPERATIVE",
+ startAt: "2023-10-10T10:00:00Z",
+ endAt: "2023-10-10T12:00:00Z"
+ };
+ try {
+ await api.post("/api/v1/schedules", invalidPayload);
+ fail("Expected to throw an error for invalid data type");
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ test("should return 401 or 403 when unauthorized or forbidden", async () => {
+ const noAuthApi = axios.create({
+ baseURL: process.env.API_BASE_URL || "http://localhost:3000"
+ // No authorization header
+ });
+
+ try {
+ await noAuthApi.post("/api/v1/schedules", validPayload);
+ fail("Expected to throw an error for unauthorized request");
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ expect([401, 403]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ test("should handle large payload gracefully", async () => {
+ const largeName = "A".repeat(10000);
+ const largePayload = {
+ ...validPayload,
+ name: largeName
+ };
+
+ try {
+ const response = await api.post("/api/v1/schedules", largePayload);
+ // The API might handle large input in various ways, e.g. success or error
+ expect([200, 400, 422]).toContain(response.status);
+ if (response.status === 200) {
+ expect(response.headers["content-type"]).toContain("application/json");
+ expect(response.data).toHaveProperty("id");
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ // For a large payload, we might also see other errors like 413 or 500
+ expect([400, 413, 422, 500]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ test("should return 400 or 422 for malformed JSON", async () => {
+ // We'll simulate a malformed request by sending an invalid JSON string
+ try {
+ const response = await api.request({
+ method: "POST",
+ url: "/api/v1/schedules",
+ data: "invalid_json",
+ headers: {
+ "Content-Type": "application/json"
+ }
+ });
+ fail("Expected to throw an error for malformed JSON");
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.py
new file mode 100644
index 0000000000..fbd6ccc810
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.py
@@ -0,0 +1,156 @@
+import axios, { AxiosInstance } from 'axios';
+import { config } from 'dotenv';
+
+// Load environment variables from .env (if present)
+config();
+
+// Retrieve base URL and auth token from environment variables
+const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+const authToken = process.env.API_AUTH_TOKEN || '';
+
+// Helper to create an axios instance with/without auth token
+function getAxiosInstance(token?: string): AxiosInstance {
+ return axios.create({
+ baseURL,
+ headers: {
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ 'Content-Type': 'application/json',
+ },
+ });
+}
+
+describe('POST /api/v1/tasks/batch - Batch trigger tasks', () => {
+ /**
+ * 1. Valid request test
+ * - Ensure the API responds with status 200.
+ * - Validate the response headers and body.
+ */
+ it('should trigger tasks successfully with a valid payload', async () => {
+ const axiosInstance = getAxiosInstance(authToken);
+ const payload = {
+ tasks: [
+ { name: 'Task 1', payload: { param: 'value1' } },
+ { name: 'Task 2', payload: { param: 'value2' } },
+ ],
+ };
+
+ const response = await axiosInstance.post('/api/v1/tasks/batch', payload);
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ // Basic response body validation
+ expect(response.data).toBeDefined();
+ // Additional schema validation checks can be added here
+ });
+
+ /**
+ * 2. Invalid request body tests
+ * - Missing or invalid request body
+ * - Expect 400 or 422 (both are acceptable per instructions).
+ */
+ it('should return 400 or 422 for missing or invalid request body', async () => {
+ const axiosInstance = getAxiosInstance(authToken);
+
+ try {
+ await axiosInstance.post('/api/v1/tasks/batch', {});
+ fail('Expected an error but request succeeded');
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response?.status);
+ expect(error.response?.headers['content-type']).toContain('application/json');
+ }
+ });
+
+ /**
+ * 3. Exceeding payload limit test
+ * - The endpoint supports up to 500 tasks.
+ * - Sending more should trigger a 400 or 422.
+ */
+ it('should return 400 or 422 when tasks exceed the limit of 500', async () => {
+ const axiosInstance = getAxiosInstance(authToken);
+ const bigArray = new Array(501).fill({ name: 'Excess Task', payload: {} });
+
+ try {
+ await axiosInstance.post('/api/v1/tasks/batch', { tasks: bigArray });
+ fail('Expected an error but request succeeded');
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response?.status);
+ expect(error.response?.headers['content-type']).toContain('application/json');
+ }
+ });
+
+ /**
+ * 4. Edge case: Empty tasks array
+ * - The API might allow an empty array or reject it.
+ * - Adjust the expected status accordingly if your API specifically disallows empty arrays.
+ */
+ it('should handle empty tasks array (boundary condition)', async () => {
+ const axiosInstance = getAxiosInstance(authToken);
+ const payload = {
+ tasks: [],
+ };
+
+ let response;
+ try {
+ response = await axiosInstance.post('/api/v1/tasks/batch', payload);
+ } catch (error: any) {
+ response = error.response;
+ }
+
+ // Depending on your API design, 200, 400, or 422 are possible
+ expect([200, 400, 422]).toContain(response?.status);
+ if (response?.status === 200) {
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toBeDefined();
+ }
+ });
+
+ /**
+ * 5. Unauthorized or forbidden tests
+ * - API might return 401 or 403 when token is missing or invalid.
+ */
+ it('should fail with 401 or 403 when no authorization token is provided', async () => {
+ const axiosInstance = getAxiosInstance(); // No token
+ const payload = {
+ tasks: [{ name: 'Task 1', payload: { param: 'value1' } }],
+ };
+
+ try {
+ await axiosInstance.post('/api/v1/tasks/batch', payload);
+ fail('Expected an error but request succeeded');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error.response?.status);
+ expect(error.response?.headers['content-type']).toContain('application/json');
+ }
+ });
+
+ it('should fail with 401 or 403 when invalid token is provided', async () => {
+ const axiosInstance = getAxiosInstance('InvalidToken123');
+ const payload = {
+ tasks: [{ name: 'Task 1', payload: { param: 'value1' } }],
+ };
+
+ try {
+ await axiosInstance.post('/api/v1/tasks/batch', payload);
+ fail('Expected an error but request succeeded');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error.response?.status);
+ expect(error.response?.headers['content-type']).toContain('application/json');
+ }
+ });
+
+ /**
+ * 6. Resource not found tests
+ * - 404 can occur if the endpoint is incorrect or resource is missing.
+ */
+ it('should return 404 for an invalid endpoint path', async () => {
+ const axiosInstance = getAxiosInstance(authToken);
+
+ try {
+ // Intentionally using a non-existent path
+ await axiosInstance.post('/api/v1/tasks/batch/does-not-exist', {});
+ fail('Expected an error but request succeeded');
+ } catch (error: any) {
+ expect(error.response?.status).toBe(404);
+ expect(error.response?.headers['content-type']).toContain('application/json');
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.py b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.py
new file mode 100644
index 0000000000..b73c5e96c4
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.py
@@ -0,0 +1,203 @@
+import axios, { AxiosResponse } from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config(); // Loads environment variables from .env file if present
+
+const baseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
+const token = process.env.API_AUTH_TOKEN || 'YOUR_DEFAULT_TOKEN';
+
+/**
+ * This test suite validates the POST /api/v1/tasks/{taskIdentifier}/trigger endpoint.
+ * It checks:
+ * 1. Input Validation (required params, correct data types, boundary/edge cases, invalid inputs => 400/422)
+ * 2. Response Validation (status codes, response body schema)
+ * 3. Response Headers (Content-Type, etc.)
+ * 4. Edge Cases & Limit Testing (large payloads, boundary values, etc.)
+ * 5. Authorization & Authentication (valid/invalid tokens => 401/403)
+ */
+describe('POST /api/v1/tasks/:taskIdentifier/trigger', () => {
+ const client = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ validateStatus: () => true, // Let us handle status codes in tests
+ });
+
+ // Helper function to simplify post requests
+ const triggerTask = async (
+ taskIdentifier: string,
+ payload: Record | undefined = {}
+ ): Promise => {
+ return client.post(`/api/v1/tasks/${taskIdentifier}/trigger`, payload);
+ };
+
+ // 1. Test valid input and expect a 200 response
+ it('should trigger a task successfully with valid identifier and payload', async () => {
+ // Replace "validTaskIdentifier" with a known valid identifier
+ const validTaskIdentifier = '1234';
+ // This sample payload would conform to the expected schema if required.
+ // If your OpenAPI schema requires other fields, update accordingly.
+ const validPayload = {
+ // Example field(s), adjust to match your schema
+ // description: 'Optional description',
+ };
+
+ const response = await triggerTask(validTaskIdentifier, validPayload);
+
+ // Expect a successful response (usually 200)
+ expect([200]).toContain(response.status);
+
+ // Check response headers
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Check response body (assuming the schema contains certain fields)
+ // Example: if TriggerTaskResponse has a "success" boolean and a "message" string
+ // Adjust to match your real schema
+ expect(response.data).toHaveProperty('success');
+ expect(typeof response.data.success).toBe('boolean');
+ if (typeof response.data.message !== 'undefined') {
+ expect(typeof response.data.message).toBe('string');
+ }
+ });
+
+ // 2. Test invalid/missing parameters and expect 400 or 422
+ it('should return 400 or 422 when the request payload is invalid', async () => {
+ // Replace "validTaskIdentifier" with a known valid identifier
+ const validTaskIdentifier = '1234';
+
+ // Example of an invalid payload that doesn't conform to the schema
+ // e.g., sending wrong data types
+ const invalidPayload = {
+ invalidField: 999, // Suppose the schema does not allow this field or requires a string
+ };
+
+ const response = await triggerTask(validTaskIdentifier, invalidPayload);
+
+ expect([400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Check the error response structure if applicable
+ // e.g., expecting error code, message
+ // Adjust to match your ErrorResponse schema
+ expect(response.data).toHaveProperty('error');
+ expect(typeof response.data.error).toBe('string');
+ });
+
+ // 3. Test required parameter in the path (taskIdentifier) is missing or invalid
+ it('should return 400 or 404 if the taskIdentifier is invalid', async () => {
+ // We can use an empty string or a malformed ID
+ const invalidTaskIdentifier = '';
+
+ const response = await triggerTask(invalidTaskIdentifier);
+
+ // Some APIs may treat empty path params as missing => 400 or 404
+ expect([400, 404]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Check error message if available
+ expect(response.data).toHaveProperty('error');
+ });
+
+ // 4. Test 401 or 403 for unauthorized/forbidden
+ it('should return 401 or 403 if the request is made without valid authorization', async () => {
+ // Create a client without proper authorization header
+ const unauthorizedClient = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ 'Content-Type': 'application/json',
+ // Omit Authorization
+ },
+ validateStatus: () => true,
+ });
+
+ // Helper function for unauthorized test
+ const triggerUnauthorizedTask = async (
+ taskIdentifier: string,
+ payload: Record | undefined = {}
+ ): Promise => {
+ return unauthorizedClient.post(
+ `/api/v1/tasks/${taskIdentifier}/trigger`,
+ payload
+ );
+ };
+
+ const response = await triggerUnauthorizedTask('1234');
+
+ // Expect 401 or 403 for unauthorized or forbidden
+ expect([401, 403]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ if (response.data) {
+ expect(response.data).toHaveProperty('error');
+ expect(typeof response.data.error).toBe('string');
+ }
+ });
+
+ // 5. Test 404 if the resource is not found
+ it('should return 404 if the taskIdentifier does not exist', async () => {
+ // Provide a taskIdentifier that is known to be non-existent
+ const nonExistentTaskIdentifier = 'non-existing-task-id';
+
+ const response = await triggerTask(nonExistentTaskIdentifier);
+
+ // Expect 404 if the resource is not found
+ expect(response.status).toBe(404);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Check the error response structure
+ expect(response.data).toHaveProperty('error');
+ expect(typeof response.data.error).toBe('string');
+ });
+
+ // 6. Test large payloads or edge case payload
+ it('should handle large payload and return appropriate response', async () => {
+ // If your endpoint supports or requires a request body, we test a large/edge payload.
+ // Adjust field names to match your schema.
+ const largePayload = {
+ data: 'X'.repeat(10000), // 10k characters
+ };
+
+ // Replace "validTaskIdentifier" with a known valid identifier
+ const validTaskIdentifier = '1234';
+ const response = await triggerTask(validTaskIdentifier, largePayload);
+
+ // Typically it should still return 200 if the payload is valid, or 400/422 if too large.
+ expect([200, 400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // If success, check output structure; if error, check error structure
+ if (response.status === 200) {
+ expect(response.data).toHaveProperty('success');
+ } else {
+ expect(response.data).toHaveProperty('error');
+ }
+ });
+
+ // 7. Test handling of server errors (simulate if possible)
+ // In practice, this might require mocking or special test setup.
+ // For completeness, we outline a test that checks 500 response.
+ it('should handle internal server errors (500) gracefully', async () => {
+ // This test is more theoretical unless you have a way to force a 500.
+ // One approach is to send a payload known to cause a server error. Adjust accordingly.
+
+ const maybeServerErrorPayload = {
+ // Some known values that cause an error in your system (if you have them)
+ causeServerCrash: true, // Example, not a real field
+ };
+
+ // Replace "validTaskIdentifier" with a valid identifier
+ const validTaskIdentifier = '1234';
+
+ const response = await triggerTask(validTaskIdentifier, maybeServerErrorPayload);
+
+ // 500 might be the expected status if a server error occurs
+ // Some APIs might respond with 400 in these cases, but we assume 500 for an internal error
+ expect([500, 400]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ if (response.data) {
+ expect(response.data).toHaveProperty('error');
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.py b/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.py
new file mode 100644
index 0000000000..410f36f65c
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.py
@@ -0,0 +1,185 @@
+import axios, { AxiosError, AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/**
+ * Comprehensive Jest test suite for the endpoint:
+ * POST /api/v2/runs/{runId}/cancel
+ *
+ * Requirements:
+ * - Loads base URL from process.env.API_BASE_URL
+ * - Loads auth token from process.env.API_AUTH_TOKEN
+ * - Uses axios for HTTP requests
+ * - Covers:
+ * 1) Input Validation
+ * 2) Response Validation
+ * 3) Response Headers Validation
+ * 4) Edge Case & Limit Testing
+ * 5) Testing Authorization & Authentication
+ *
+ * Usage:
+ * - Set environment variables:
+ * API_BASE_URL => e.g., https://example.com
+ * API_AUTH_TOKEN => e.g., someValidToken
+ * - Run: npx jest (or npm test / yarn test)
+ */
+
+describe('POST /api/v2/runs/{runId}/cancel', () => {
+ const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ const validAuthToken = process.env.API_AUTH_TOKEN || 'VALID_TOKEN';
+
+ /**
+ * Creates an axios instance without default Authorization header.
+ * Use this for tests that specifically require missing credentials.
+ */
+ const axiosInstanceWithoutAuth = axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ validateStatus: () => true, // We'll handle status codes manually
+ });
+
+ /**
+ * Creates an axios instance with a (potentially valid or invalid) auth token.
+ * @param token The token to use in the Authorization header.
+ */
+ function createAxiosInstanceWithToken(token: string) {
+ return axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ validateStatus: () => true, // We'll handle status codes manually
+ });
+ }
+
+ /**
+ * Helper to check JSON content-type.
+ */
+ function expectJsonContentType(response: AxiosResponse) {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ }
+
+ /**
+ * Helper to ensure the response body matches the error schema.
+ * We expect an object with at least an 'error' field.
+ */
+ function expectErrorSchema(body: any) {
+ expect(body).toBeDefined();
+ expect(body).toHaveProperty('error');
+ expect(typeof body.error).toBe('string');
+ }
+
+ /**
+ * Helper to ensure the response body matches the success schema:
+ * { id: string }
+ */
+ function expectSuccessSchema(body: any) {
+ expect(body).toBeDefined();
+ expect(body).toHaveProperty('id');
+ expect(typeof body.id).toBe('string');
+ }
+
+ let axiosInstanceWithValidToken: ReturnType;
+ let axiosInstanceWithInvalidToken: ReturnType;
+
+ beforeAll(() => {
+ axiosInstanceWithValidToken = createAxiosInstanceWithToken(validAuthToken);
+ axiosInstanceWithInvalidToken = createAxiosInstanceWithToken('INVALID_TOKEN');
+ });
+
+ /**
+ * 1) Authorization & Authentication Tests
+ */
+ it('should return 401 or 403 if the Authorization header is missing', async () => {
+ const response = await axiosInstanceWithoutAuth.post('/api/v2/runs/someRunId/cancel');
+ expect([401, 403]).toContain(response.status);
+ expectJsonContentType(response);
+ expectErrorSchema(response.data);
+ });
+
+ it('should return 401 or 403 if the token is invalid', async () => {
+ const response = await axiosInstanceWithInvalidToken.post('/api/v2/runs/someRunId/cancel');
+ expect([401, 403]).toContain(response.status);
+ expectJsonContentType(response);
+ expectErrorSchema(response.data);
+ });
+
+ /**
+ * 2) Input Validation Tests
+ * - We test different forms of invalid "runId" in the path.
+ * - The API might return 400 or 422 for invalid request.
+ */
+ it('should return 400 or 422 for an empty runId', async () => {
+ const response = await axiosInstanceWithValidToken.post('/api/v2/runs//cancel');
+ expect([400, 422]).toContain(response.status);
+ if (response.status !== 204) {
+ expectJsonContentType(response);
+ expectErrorSchema(response.data);
+ }
+ });
+
+ it('should return 400 or 422 for an invalid runId format', async () => {
+ const invalidRunId = '???###';
+ const response = await axiosInstanceWithValidToken.post(`/api/v2/runs/${invalidRunId}/cancel`);
+ expect([400, 422]).toContain(response.status);
+ if (response.status !== 204) {
+ expectJsonContentType(response);
+ expectErrorSchema(response.data);
+ }
+ });
+
+ it('should return 400 or 422 for a very large runId', async () => {
+ const largeRunId = 'run_' + 'x'.repeat(10000);
+ const response = await axiosInstanceWithValidToken.post(`/api/v2/runs/${largeRunId}/cancel`);
+ expect([400, 422]).toContain(response.status);
+ if (response.status !== 204) {
+ expectJsonContentType(response);
+ expectErrorSchema(response.data);
+ }
+ });
+
+ /**
+ * 3) Resource Not Found Test (404)
+ * - If the run does not exist, we expect 404.
+ */
+ it('should return 404 if the runId does not exist', async () => {
+ const nonExistentId = 'run_nonexistent_123';
+ const response = await axiosInstanceWithValidToken.post(`/api/v2/runs/${nonExistentId}/cancel`);
+ expect(response.status).toBe(404);
+ expectJsonContentType(response);
+ expectErrorSchema(response.data);
+ expect(response.data.error).toBe('Run not found');
+ });
+
+ /**
+ * 4) Successful Cancellation (200)
+ * - For a valid run in progress or if run is already completed,
+ * we expect 200 with { id: string }.
+ */
+ it('should return 200 and valid JSON schema if the runId is valid and run is cancellable or completed', async () => {
+ // Replace "liveRunId" with an actual running or known ID in your environment.
+ // If the run is completed, the API states it will have no effect but still succeed.
+ const liveRunId = 'run_existing_1234';
+
+ const response = await axiosInstanceWithValidToken.post(`/api/v2/runs/${liveRunId}/cancel`);
+ expect(response.status).toBe(200);
+ expectJsonContentType(response);
+ expectSuccessSchema(response.data);
+ });
+
+ /**
+ * 5) Verify Response Headers for All Valid Cases
+ * - We already call expectJsonContentType(response) in each test,
+ * but here is an explicit test for 200 case.
+ */
+ it('should include appropriate headers (Content-Type: application/json) on success', async () => {
+ const liveRunId = 'run_existing_5678'; // replace with a valid run
+
+ const response = await axiosInstanceWithValidToken.post(`/api/v2/runs/${liveRunId}/cancel`);
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ // Additional headers like Cache-Control, X-RateLimit, etc. can be tested if applicable.
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.py b/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.py
new file mode 100644
index 0000000000..feb26c8edf
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.py
@@ -0,0 +1,158 @@
+import axios, { AxiosResponse } from 'axios';
+import { config as dotenvConfig } from 'dotenv';
+import { describe, it, expect } from '@jest/globals';
+
+dotenvConfig();
+
+const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
+const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN || '';
+
+/**
+ * Jest test suite for PUT /api/v1/projects/{projectRef}/envvars/{env}/{name}
+ *
+ * Summary:
+ * Update a specific environment variable for a specific project and environment.
+ *
+ * This suite tests:
+ * 1. Input Validation (required params, body data types/edge cases).
+ * 2. Response Validation (status codes, response body schema).
+ * 3. Response Headers Validation (Content-Type and other headers).
+ * 4. Edge & Limit cases (large payloads, empty values).
+ * 5. Authorization & Authentication testing (valid, invalid, missing tokens).
+ */
+
+describe('PUT /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
+ const validProjectRef = 'my-project';
+ const validEnv = 'staging';
+ const validName = 'TEST_VARIABLE';
+
+ // A guessed valid payload structure based on the assumption that the endpoint expects
+ // a JSON body like: { value: string }
+ const validPayload = {
+ value: 'UPDATED_VALUE'
+ };
+
+ // Invalid payload - missing or incorrect fields
+ const invalidPayload = {
+ invalidKey: 123
+ };
+
+ // Axios default config with auth header
+ const defaultAxiosConfig = {
+ headers: {
+ Authorization: `Bearer ${API_AUTH_TOKEN}`,
+ 'Content-Type': 'application/json'
+ },
+ validateStatus: () => true // Allow handling of non-2xx responses in tests
+ };
+
+ // Helper to build endpoint URL
+ function buildUrl(projectRef: string, env: string, name: string): string {
+ return `${API_BASE_URL}/api/v1/projects/${projectRef}/envvars/${env}/${name}`;
+ }
+
+ it('should update environment variable successfully with valid payload', async () => {
+ const url = buildUrl(validProjectRef, validEnv, validName);
+
+ const response: AxiosResponse = await axios.put(url, validPayload, defaultAxiosConfig);
+
+ // Expect a 200 success response
+ expect(response.status).toBe(200);
+
+ // Validate response headers
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate response body (schema check can be more detailed if we have a schema)
+ expect(response.data).toBeDefined();
+ // Example: expect(response.data).toHaveProperty('message');
+ });
+
+ it('should return 400 or 422 for invalid body/payload', async () => {
+ const url = buildUrl(validProjectRef, validEnv, validName);
+
+ const response: AxiosResponse = await axios.put(url, invalidPayload, defaultAxiosConfig);
+
+ // Expect a 400 or 422 for invalid request body
+ expect([400, 422]).toContain(response.status);
+
+ // Validate response headers
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate error response body
+ expect(response.data).toBeDefined();
+ });
+
+ it('should return 400 if required path params are missing or invalid', async () => {
+ // Attempt to call with invalid (empty) projectRef or env
+ const url = buildUrl('', validEnv, validName);
+
+ const response: AxiosResponse = await axios.put(url, validPayload, defaultAxiosConfig);
+
+ // Expect a 400 or 404 here, depending on the API implementation
+ // but typically a missing/invalid path param could lead to 400 or 404
+ expect([400, 404]).toContain(response.status);
+ expect(response.data).toBeDefined();
+ });
+
+ it('should return 401 or 403 for unauthorized/forbidden requests', async () => {
+ // Remove the token from headers
+ const noAuthConfig = {
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ validateStatus: () => true
+ };
+
+ const url = buildUrl(validProjectRef, validEnv, validName);
+ const response: AxiosResponse = await axios.put(url, validPayload, noAuthConfig);
+
+ // Expect a 401 or 403 if no valid token is present
+ expect([401, 403]).toContain(response.status);
+ expect(response.data).toBeDefined();
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should return 404 if the resource is not found', async () => {
+ // Use an obviously invalid environment variable name
+ const url = buildUrl(validProjectRef, validEnv, 'NON_EXISTENT_VARIABLE');
+
+ const response: AxiosResponse = await axios.put(url, validPayload, defaultAxiosConfig);
+
+ // Expect a 404 if the environment variable does not exist
+ expect(response.status).toBe(404);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toBeDefined();
+ });
+
+ it('should handle large payload (edge case testing)', async () => {
+ // Construct a large string
+ const largeString = 'x'.repeat(5000);
+ const largePayload = {
+ value: largeString
+ };
+
+ const url = buildUrl(validProjectRef, validEnv, validName);
+ const response: AxiosResponse = await axios.put(url, largePayload, defaultAxiosConfig);
+
+ // Depending on implementation, can be success if large payload is allowed, or 400/413 if too large
+ // Adjust expectations based on typical API behavior
+ expect([200, 400, 413]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toBeDefined();
+ });
+
+ it('should return 400 or 422 when sending empty value in request body if not allowed', async () => {
+ const emptyValuePayload = {
+ value: ''
+ };
+ const url = buildUrl(validProjectRef, validEnv, validName);
+
+ const response: AxiosResponse = await axios.put(url, emptyValuePayload, defaultAxiosConfig);
+
+ // If the API does not allow empty strings for value, it might return 400 or 422
+ // If the API does allow it, adjust the expectation accordingly
+ expect([200, 400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toBeDefined();
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.py b/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.py
new file mode 100644
index 0000000000..b13636c8da
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.py
@@ -0,0 +1,220 @@
+import axios, { AxiosError, AxiosResponse } 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;
+
+// Interface for a successful response
+interface UpdateRunMetadataResponse {
+ metadata: Record;
+}
+
+// Interface for an error response
+interface ErrorResponse {
+ error: string;
+}
+
+describe('PUT /api/v1/runs/{runId}/metadata', () => {
+ // Helper to create Axios instance with base config
+ const createAxiosInstance = (token?: string) => {
+ return axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ validateStatus: () => true, // We will handle status checks in the tests
+ });
+ };
+
+ const validRunId = '123';
+ const nonExistentRunId = '999999';
+
+ /**
+ * Valid metadata payload to update
+ */
+ const validMetadataPayload = {
+ metadata: {
+ key1: 'value1',
+ key2: 2,
+ },
+ };
+
+ /**
+ * Creates a very large metadata object for edge-case testing.
+ */
+ const largeMetadataPayload = {
+ metadata: {
+ largeKey: 'x'.repeat(10_000),
+ },
+ };
+
+ describe('Input Validation & Response Validation', () => {
+ test('should update run metadata with valid inputs (200)', async () => {
+ const instance = createAxiosInstance(API_AUTH_TOKEN);
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${validRunId}/metadata`,
+ validMetadataPayload
+ );
+
+ // Expecting successful update
+ expect([200]).toContain(response.status);
+ expect(response.data).toHaveProperty('metadata');
+ expect(typeof response.data.metadata).toBe('object');
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ test('should return 400 or 422 when runId is invalid (string, empty, etc.)', async () => {
+ const instance = createAxiosInstance(API_AUTH_TOKEN);
+ // Trying a clearly invalid runId
+ const invalidRunId = 'abc';
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${invalidRunId}/metadata`,
+ validMetadataPayload
+ );
+
+ expect([400, 422]).toContain(response.status);
+ // The response body might contain an "error" field with a relevant message.
+ if (response.data) {
+ expect(response.data).toHaveProperty('error');
+ }
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ test('should return 400 or 422 if metadata payload is invalid (e.g., missing metadata)', async () => {
+ const instance = createAxiosInstance(API_AUTH_TOKEN);
+ // Invalid payload: missing or empty metadata field
+ const invalidPayload = { badField: 'not metadata' };
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${validRunId}/metadata`,
+ invalidPayload
+ );
+
+ expect([400, 422]).toContain(response.status);
+ if (response.data) {
+ expect(response.data).toHaveProperty('error');
+ }
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ test('should return 404 if runId does not exist', async () => {
+ const instance = createAxiosInstance(API_AUTH_TOKEN);
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${nonExistentRunId}/metadata`,
+ validMetadataPayload
+ );
+
+ // Expecting 404 for run not found
+ expect(response.status).toBe(404);
+ if (response.data) {
+ expect(response.data).toHaveProperty('error');
+ }
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ test('should handle large payload without errors', async () => {
+ const instance = createAxiosInstance(API_AUTH_TOKEN);
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${validRunId}/metadata`,
+ largeMetadataPayload
+ );
+
+ // Some APIs might handle large payload differently, but assume success is 200
+ expect([200, 400, 422]).toContain(response.status);
+ // If success, ensure metadata is returned
+ if (response.status === 200) {
+ const successData = response.data as UpdateRunMetadataResponse;
+ expect(successData).toHaveProperty('metadata');
+ } else {
+ // If not success, check we got an error structure
+ const errorData = response.data as ErrorResponse;
+ expect(errorData).toHaveProperty('error');
+ }
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ test('should handle empty metadata as valid or invalid (depending on API spec)', async () => {
+ const instance = createAxiosInstance(API_AUTH_TOKEN);
+
+ // Some APIs accept an empty object, some do not.
+ const emptyMetadataPayload = {
+ metadata: {},
+ };
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${validRunId}/metadata`,
+ emptyMetadataPayload
+ );
+
+ // Expect success or an input error
+ expect([200, 400, 422]).toContain(response.status);
+ if (response.status === 200) {
+ const successData = response.data as UpdateRunMetadataResponse;
+ expect(successData).toHaveProperty('metadata');
+ } else {
+ const errorData = response.data as ErrorResponse;
+ expect(errorData).toHaveProperty('error');
+ }
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+ });
+
+ describe('Testing Authorization & Authentication', () => {
+ test('should return 401 or 403 if API token is missing', async () => {
+ const instance = createAxiosInstance(undefined); // no token
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${validRunId}/metadata`,
+ validMetadataPayload
+ );
+
+ expect([401, 403]).toContain(response.status);
+ if (response.status === 401 || response.status === 403) {
+ expect(response.data).toHaveProperty('error');
+ }
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ test('should return 401 or 403 if API token is invalid', async () => {
+ const instance = createAxiosInstance('invalid-token');
+
+ const response: AxiosResponse = await instance.put(
+ `/api/v1/runs/${validRunId}/metadata`,
+ validMetadataPayload
+ );
+
+ expect([401, 403]).toContain(response.status);
+ if (response.status === 401 || response.status === 403) {
+ expect(response.data).toHaveProperty('error');
+ }
+
+ // Header validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+ });
+
+ // Additional tests could cover server errors or other edge cases (e.g., 500 responses).
+});
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.py b/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.py
new file mode 100644
index 0000000000..9ea7c12429
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.py
@@ -0,0 +1,185 @@
+import axios, { AxiosInstance } from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+describe('PUT /api/v1/schedules/{schedule_id}', () => {
+ let axiosInstance: AxiosInstance;
+ const baseURL = process.env.API_BASE_URL;
+ const token = process.env.API_AUTH_TOKEN;
+
+ // You can replace these with actual valid/invalid IDs from your system.
+ const validScheduleId = 'test-schedule-id';
+ const invalidScheduleId = '';
+
+ // Example of a valid request payload (adjust keys/values based on your API's schema)
+ const validPayload = {
+ name: 'Updated Schedule Name',
+ type: 'IMPERATIVE',
+ tasks: [
+ {
+ action: 'someAction',
+ parameters: {}
+ }
+ ]
+ };
+
+ // Example of an invalid request payload
+ const invalidPayload = {
+ // For instance, name is empty, type is wrong data type, tasks is not an array
+ name: '',
+ type: 1234,
+ tasks: 'not-an-array'
+ };
+
+ beforeAll(() => {
+ axiosInstance = axios.create({
+ baseURL,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ });
+
+ describe('Input Validation', () => {
+ test('should update schedule successfully with valid parameters', async () => {
+ expect(baseURL).toBeDefined();
+ expect(token).toBeDefined();
+
+ const response = await axiosInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ validPayload
+ );
+
+ expect(response.status).toBe(200);
+ // Additional checks on response body to match the expected schema
+ expect(response.data).toHaveProperty('id', validScheduleId);
+ expect(response.data).toHaveProperty('name', validPayload.name);
+ expect(response.data).toHaveProperty('type', validPayload.type);
+ // Further validation on tasks if needed
+ });
+
+ test('should return 400 or 422 when payload is invalid', async () => {
+ try {
+ await axiosInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ invalidPayload
+ );
+ fail('Request should have failed with 400 or 422.');
+ } catch (error: any) {
+ const statusCode = error?.response?.status;
+ expect([400, 422]).toContain(statusCode);
+ }
+ });
+
+ test('should return 404 if schedule_id does not exist', async () => {
+ const nonExistentId = 'nonexistent-schedule-id';
+ try {
+ await axiosInstance.put(
+ `/api/v1/schedules/${nonExistentId}`,
+ validPayload
+ );
+ fail('Request should have failed with 404.');
+ } catch (error: any) {
+ expect(error?.response?.status).toBe(404);
+ }
+ });
+
+ test('should return 400 or 422 if schedule_id is invalid (e.g., empty)', async () => {
+ try {
+ await axiosInstance.put(
+ `/api/v1/schedules/${invalidScheduleId}`,
+ validPayload
+ );
+ fail('Request should have failed with 400 or 422.');
+ } catch (error: any) {
+ const statusCode = error?.response?.status;
+ expect([400, 422]).toContain(statusCode);
+ }
+ });
+ });
+
+ describe('Response Validation', () => {
+ test('should have the correct response headers', async () => {
+ const response = await axiosInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ validPayload
+ );
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ test('should return a valid ScheduleObject in the response body', async () => {
+ const response = await axiosInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ validPayload
+ );
+ // Check for required fields
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('name');
+ expect(response.data).toHaveProperty('type');
+ // Additional checks can be added based on the ScheduleObject schema
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ test('should handle large payload without error', async () => {
+ // Example of a large payload; adjust as needed.
+ const largePayload = {
+ name: 'A'.repeat(10000),
+ type: 'IMPERATIVE',
+ tasks: Array(1000).fill({ action: 'someAction', parameters: {} })
+ };
+
+ // We expect either a success (200) if the API can handle it,
+ // or a 400/413/422 if it's too large.
+ try {
+ const response = await axiosInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ largePayload
+ );
+ expect(response.status).toBe(200);
+ } catch (error: any) {
+ const statusCode = error?.response?.status;
+ expect([400, 413, 422]).toContain(statusCode);
+ }
+ });
+
+ test('should handle empty request body', async () => {
+ try {
+ await axiosInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ {}
+ );
+ fail('Request should have failed with 400 or 422 due to empty body.');
+ } catch (error: any) {
+ const statusCode = error?.response?.status;
+ expect([400, 422]).toContain(statusCode);
+ }
+ });
+ });
+
+ describe('Testing Authorization & Authentication', () => {
+ test('should return 401 or 403 if no token is provided', async () => {
+ const unauthorizedInstance = axios.create({ baseURL });
+ try {
+ await unauthorizedInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ validPayload
+ );
+ fail('Request should have failed with 401 or 403.');
+ } catch (error: any) {
+ const statusCode = error?.response?.status;
+ expect([401, 403]).toContain(statusCode);
+ }
+ });
+
+ test('should return 200 when valid token is provided', async () => {
+ const response = await axiosInstance.put(
+ `/api/v1/schedules/${validScheduleId}`,
+ validPayload
+ );
+ expect(response.status).toBe(200);
+ });
+ });
+});