diff --git a/README.md b/README.md
index 4e3416c4aa..4a5d4773bc 100644
--- a/README.md
+++ b/README.md
@@ -89,3 +89,5 @@ To setup and develop locally or contribute to the open source project, follow ou
+
+## Test
diff --git a/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts b/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
new file mode 100644
index 0000000000..452c023e83
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_delete_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
@@ -0,0 +1,163 @@
+import axios, { AxiosInstance, AxiosResponse } from 'axios';
+import { describe, beforeAll, test, expect } from '@jest/globals';
+
+// Example interfaces based on the provided OpenAPI schema references.
+// Adjust or enrich these according to your actual schemas.
+interface SucceedResponse {
+ success: boolean;
+ message: string;
+}
+
+interface ErrorResponse {
+ error: string;
+ message: string;
+}
+
+// Utility function for checking if a string is empty or has only whitespace.
+// Used to test edge cases with path parameters.
+function isEmptyOrWhitespace(str: string): boolean {
+ return !str || !str.trim();
+}
+
+// Test suite for DELETE /api/v1/projects/{projectRef}/envvars/{env}/{name}
+describe('DELETE /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
+ let axiosInstance: AxiosInstance;
+
+ // Set up axios instance before tests.
+ beforeAll(() => {
+ axiosInstance = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ // Let the test handle status codes, so we set validateStatus to always return true.
+ validateStatus: () => true,
+ });
+ });
+
+ test('Should delete environment variable successfully (200)', async () => {
+ // Arrange
+ const projectRef = 'my-project';
+ const env = 'development';
+ const name = 'TEST_VAR';
+
+ // Act
+ const response: AxiosResponse = await axiosInstance.delete(
+ `/api/v1/projects/${projectRef}/envvars/${env}/${name}`,
+ {
+ headers: {
+ 'Authorization': `Bearer ${process.env.API_AUTH_TOKEN}`,
+ },
+ }
+ );
+
+ // Assert: 200 Success
+ // The API specification indicates a 200 response, but if the server returns 200 or 204, adapt the check.
+ expect([200]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ if (response.status === 200) {
+ // Validate that the response body conforms to SucceedResponse if status is 200.
+ const data = response.data as SucceedResponse;
+ expect(data).toHaveProperty('success');
+ expect(data).toHaveProperty('message');
+ }
+ });
+
+ test('Should return 400 (Or 422) for invalid path parameters', async () => {
+ // Arrange: Use invalid path parameters (e.g., empty or malformed)
+ const invalidProjectRef = '';
+ const invalidEnv = '';
+ const invalidName = '';
+
+ // Act
+ const response: AxiosResponse = await axiosInstance.delete(
+ `/api/v1/projects/${invalidProjectRef}/envvars/${invalidEnv}/${invalidName}`,
+ {
+ headers: {
+ 'Authorization': `Bearer ${process.env.API_AUTH_TOKEN}`,
+ },
+ }
+ );
+
+ // Assert: Expect 400 or 422 for invalid input
+ // The API may return 400 or 422 for invalid payload or path parameters.
+ expect([400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate error response structure
+ const data = response.data;
+ expect(data).toHaveProperty('error');
+ expect(data).toHaveProperty('message');
+
+ // Further checks on the error content could be done here.
+ });
+
+ test('Should return 401 or 403 if authorization token is missing or invalid', async () => {
+ // Arrange
+ const projectRef = 'my-project';
+ const env = 'development';
+ const name = 'ANOTHER_TEST_VAR';
+
+ // Act
+ // Intentionally omit or use an invalid token.
+ const response: AxiosResponse = await axiosInstance.delete(
+ `/api/v1/projects/${projectRef}/envvars/${env}/${name}`
+ // No headers provided to simulate missing Authorization
+ );
+
+ // Assert: Check 401 or 403. API may return either for unauthorized/forbidden.
+ expect([401, 403]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate error response.
+ const data = response.data;
+ expect(data).toHaveProperty('error');
+ expect(data).toHaveProperty('message');
+ });
+
+ test('Should return 404 for non-existent environment variable', async () => {
+ // Arrange
+ const projectRef = 'my-project';
+ const env = 'development';
+ const name = 'NON_EXISTENT_VAR';
+
+ // Act
+ const response: AxiosResponse = await axiosInstance.delete(
+ `/api/v1/projects/${projectRef}/envvars/${env}/${name}`,
+ {
+ headers: {
+ 'Authorization': `Bearer ${process.env.API_AUTH_TOKEN}`,
+ },
+ }
+ );
+
+ // Assert: Expect 404 if the resource is not found.
+ expect(response.status).toBe(404);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate error response.
+ const data = response.data;
+ expect(data).toHaveProperty('error');
+ expect(data).toHaveProperty('message');
+ });
+
+ test('Should handle extremely large path parameters gracefully (edge case)', async () => {
+ // Arrange: Create an extremely large string.
+ const largeString = 'x'.repeat(5000); // 5,000 characters
+ const projectRef = largeString;
+ const env = largeString;
+ const name = largeString;
+
+ const response: AxiosResponse = await axiosInstance.delete(
+ `/api/v1/projects/${projectRef}/envvars/${env}/${name}`,
+ {
+ headers: {
+ 'Authorization': `Bearer ${process.env.API_AUTH_TOKEN}`,
+ },
+ }
+ );
+
+ // The API might respond with 400, 414 (URI Too Long), or similar.
+ // If it does not handle large inputs, it could return a server error (500+).
+ // Adjust expectations based on actual API behavior.
+ expect([400, 414, 422, 500]).toContain(response.status);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.ts b/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.ts
new file mode 100644
index 0000000000..1d52b979e1
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_delete_api-v1-schedules-{schedule_id}.ts
@@ -0,0 +1,113 @@
+import axios, { AxiosInstance, AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
+
+/**
+ * Below tests focus on DELETE /api/v1/schedules/{schedule_id}
+ * using Jest (test framework) + axios (HTTP client) in TypeScript.
+ *
+ * Make sure to:
+ * 1. Set process.env.API_BASE_URL to your API base URL.
+ * 2. Set process.env.API_AUTH_TOKEN to a valid auth token for authorization.
+ * 3. Provide a valid IMPERATIVE schedule ID below if you want to test 200 success.
+ * (or create one in a setup step if needed.)
+ */
+
+// Example schedule IDs for testing. Modify these to valid/invalid values in your environment.
+// The validScheduleId should reference an existing "IMPERATIVE" schedule.
+const validScheduleId = 'YOUR_VALID_IMPERATIVE_SCHEDULE_ID';
+// A schedule ID that does not exist.
+const nonExistentScheduleId = 'nonexistent-schedule-id';
+// A malformed schedule ID.
+const invalidScheduleId = '!!!';
+
+// Utility function to create an Axios instance.
+function createApiClient(token?: string): AxiosInstance {
+ return axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: token ? `Bearer ${token}` : '',
+ },
+ validateStatus: () => true, // allow us to handle status codes ourselves
+ });
+}
+
+// Main test suite
+describe('DELETE /api/v1/schedules/{schedule_id}', () => {
+ let apiClient: AxiosInstance;
+
+ beforeAll(() => {
+ // Create a client with valid auth token
+ apiClient = createApiClient(process.env.API_AUTH_TOKEN);
+ });
+
+ // 1. Input Validation: Missing or invalid parameters
+
+ it('should return 404 (or possibly 400) when schedule_id is empty', async () => {
+ // Attempt to DELETE with an empty schedule ID (effectively /api/v1/schedules/)
+ const response: AxiosResponse = await apiClient.delete('/api/v1/schedules/');
+ // Depending on the server’s configuration, this might return 404, 400, or another error.
+ // We expect an error since the path is incomplete.
+ expect(response.status).toBeGreaterThanOrEqual(400);
+ // Some servers might treat it as 404 Not Found.
+ // If your API returns 400 or 422 for invalid path params, adapt expectations accordingly.
+ });
+
+ it('should return 400 or 422 for a malformed schedule_id', async () => {
+ const response: AxiosResponse = await apiClient.delete(`/api/v1/schedules/${invalidScheduleId}`);
+ // Many APIs will respond with 400 or 422 for invalid ID formats.
+ expect([400, 422]).toContain(response.status);
+ });
+
+ // 2. Response Validation (200 success, 404 Not Found, etc.)
+
+ it('should delete schedule successfully (200) for a valid IMPERATIVE schedule_id', async () => {
+ // If validScheduleId references an existing schedule, we expect a 200.
+ // This test will fail if that schedule does not exist.
+ const response: AxiosResponse = await apiClient.delete(`/api/v1/schedules/${validScheduleId}`);
+ // Check status code
+ expect(response.status).toBe(200);
+ // If the response body has a schema, verify required fields.
+ // e.g., expect(response.data).toHaveProperty('message', 'Schedule deleted successfully');
+
+ // 3. Response Headers Validation
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ // If other headers are relevant, check them here.
+ });
+
+ it('should return 404 Not Found for a non-existent schedule_id', async () => {
+ const response: AxiosResponse = await apiClient.delete(`/api/v1/schedules/${nonExistentScheduleId}`);
+ expect(response.status).toBe(404);
+ // Optionally check the response body for error details, if defined.
+ // e.g. expect(response.data).toHaveProperty('error', 'Resource not found');
+ });
+
+ // 4. Edge Case & Limit Testing
+
+ it('should handle extremely large schedule_id gracefully', async () => {
+ const largeScheduleId = 'a'.repeat(1000); // artificially large string
+ const response: AxiosResponse = await apiClient.delete(`/api/v1/schedules/${largeScheduleId}`);
+ // Could be 400, 404, or 414 (URI Too Long) depending on server config.
+ expect(response.status).toBeGreaterThanOrEqual(400);
+ });
+
+ // 5. Testing Authorization & Authentication
+
+ it('should return 401 or 403 when no auth token is provided', async () => {
+ const unauthorizedClient = createApiClient(); // no token
+ const response: AxiosResponse = await unauthorizedClient.delete(`/api/v1/schedules/${validScheduleId}`);
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should return 401 or 403 for an invalid auth token', async () => {
+ const invalidAuthClient = createApiClient('invalid-token');
+ const response: AxiosResponse = await invalidAuthClient.delete(`/api/v1/schedules/${validScheduleId}`);
+ expect([401, 403]).toContain(response.status);
+ });
+
+ // Additional tests could be added for server errors (e.g., 500) if you have a way to trigger them.
+
+ afterAll(() => {
+ // Cleanup or restore any resources if needed
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
new file mode 100644
index 0000000000..319b0dd1d1
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
@@ -0,0 +1 @@
+import axios, { AxiosInstance } from 'axios';\nimport { describe, it, beforeAll, afterAll, expect } from '@jest/globals';\n\ndescribe('GET /api/v1/projects/:projectRef/envvars/:env/:name', () => {\n let client: AxiosInstance;\n const validProjectRef = 'my-project';\n const validEnv = 'production';\n const validName = 'SECRET_KEY';\n const invalidProjectRef = '';\n const invalidEnv = '';\n const invalidName = '';\n\n beforeAll(() => {\n const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';\n const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN || 'test-token';\n\n client = axios.create({\n baseURL: API_BASE_URL,\n headers: {\n Authorization: 'Bearer ' + API_AUTH_TOKEN,\n 'Content-Type': 'application/json',\n },\n validateStatus: () => true, // We'll handle status checks in tests\n });\n });\n\n afterAll(() => {\n // any cleanup if needed\n });\n\n describe('Valid Request Scenarios', () => {\n it('should retrieve environment variable when valid parameters are provided', async () => {\n const response = await client.get(\n '/api/v1/projects/' + validProjectRef + '/envvars/' + validEnv + '/' + validName\n );\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n // Validate response body structure\n // For demonstration, we do minimal checks; ideally, you check fields from #/components/schemas/EnvVarValue\n expect(response.data).toHaveProperty('name');\n expect(response.data.name).toBe(validName);\n });\n });\n\n describe('Input Validation', () => {\n it('should return 400 or 422 for an invalid projectRef', async () => {\n const response = await client.get(\n '/api/v1/projects/' + invalidProjectRef + '/envvars/' + validEnv + '/' + validName\n );\n\n expect([400, 422]).toContain(response.status);\n expect(response.headers['content-type']).toContain('application/json');\n // check error schema\n expect(response.data).toHaveProperty('error');\n });\n\n it('should return 400 or 422 for an invalid env', async () => {\n const response = await client.get(\n '/api/v1/projects/' + validProjectRef + '/envvars/' + invalidEnv + '/' + validName\n );\n\n expect([400, 422]).toContain(response.status);\n expect(response.headers['content-type']).toContain('application/json');\n expect(response.data).toHaveProperty('error');\n });\n\n it('should return 400 or 422 for an invalid name', async () => {\n const response = await client.get(\n '/api/v1/projects/' + validProjectRef + '/envvars/' + validEnv + '/' + invalidName\n );\n\n expect([400, 422]).toContain(response.status);\n expect(response.headers['content-type']).toContain('application/json');\n expect(response.data).toHaveProperty('error');\n });\n });\n\n describe('Unauthorized & Forbidden Requests', () => {\n it('should return 401 or 403 when no authorization token is provided', async () => {\n const response = await axios.get(\n (process.env.API_BASE_URL || 'http://localhost:3000') +\n '/api/v1/projects/' + validProjectRef + '/envvars/' + validEnv + '/' + validName\n ); // no auth header\n\n expect([401, 403]).toContain(response.status);\n });\n\n it('should return 401 or 403 when an invalid authorization token is provided', async () => {\n const clientWithInvalidToken = axios.create({\n baseURL: process.env.API_BASE_URL || 'http://localhost:3000',\n headers: {\n Authorization: 'Bearer invalid_token',\n 'Content-Type': 'application/json',\n },\n validateStatus: () => true,\n });\n const response = await clientWithInvalidToken.get(\n '/api/v1/projects/' + validProjectRef + '/envvars/' + validEnv + '/' + validName\n );\n\n expect([401, 403]).toContain(response.status);\n });\n });\n\n describe('Resource Not Found', () => {\n it('should return 404 if the environment variable does not exist', async () => {\n const nonExistentName = 'NON_EXISTENT_VAR';\n const response = await client.get(\n '/api/v1/projects/' + validProjectRef + '/envvars/' + validEnv + '/' + nonExistentName\n );\n\n expect(response.status).toBe(404);\n expect(response.headers['content-type']).toContain('application/json');\n expect(response.data).toHaveProperty('error');\n });\n\n it('should return 404 if the projectRef does not exist', async () => {\n const fakeProjectRef = 'fakeProject';\n const response = await client.get(\n '/api/v1/projects/' + fakeProjectRef + '/envvars/' + validEnv + '/' + validName\n );\n\n expect(response.status).toBe(404);\n expect(response.headers['content-type']).toContain('application/json');\n expect(response.data).toHaveProperty('error');\n });\n });\n\n describe('Response Headers Validation', () => {\n it('should include general and security headers in the response', async () => {\n const response = await client.get(\n '/api/v1/projects/' + validProjectRef + '/envvars/' + validEnv + '/' + validName\n );\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n // if the API includes caching or rate-limiting headers, check them\n // e.g., expect(response.headers).toHaveProperty('cache-control');\n // e.g., expect(response.headers).toHaveProperty('x-ratelimit-limit');\n });\n });\n\n describe('Edge Case & Stress Testing', () => {\n it('should handle extremely long envvar name gracefully (expecting 400/422 or 404)', async () => {\n const longName = 'A'.repeat(1024);\n const response = await client.get(\n '/api/v1/projects/' + validProjectRef + '/envvars/' + validEnv + '/' + longName\n );\n\n // Depending on implementation, might be 400, 422, or 404\n expect([400, 422, 404]).toContain(response.status);\n });\n });\n});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.ts
new file mode 100644
index 0000000000..500d4e858b
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-envvars-{env}.ts
@@ -0,0 +1,198 @@
+import axios, { AxiosError, AxiosRequestConfig } from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+// Load environment variables
+const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
+const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN || '';
+
+// Utility function to create an Axios instance
+function createApiClient(token?: string) {
+ const config: AxiosRequestConfig = {
+ baseURL: API_BASE_URL,
+ headers: {},
+ validateStatus: () => true, // We'll handle status code checks manually
+ };
+
+ if (token) {
+ config.headers = {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ };
+ }
+
+ return axios.create(config);
+}
+
+// Example data for valid path parameter values
+// Adjust these appropriately for your API's valid data
+const VALID_PROJECT_REF = 'exampleProject';
+const VALID_ENV = 'production';
+
+// Example data for invalid path parameter values
+const INVALID_PROJECT_REF = ''; // empty string
+const INVALID_ENV = '!!!invalid-env!!!';
+const NON_EXISTENT_PROJECT_REF = 'nonExistentProject';
+const NON_EXISTENT_ENV = 'unknownEnv';
+
+// Helper function to check if response headers match the expected values
+function validateResponseHeaders(headers: any) {
+ expect(headers).toBeDefined();
+ // Content-Type should be application/json.
+ expect(headers['content-type']).toContain('application/json');
+ // Add other header checks here if needed, e.g., Cache-Control, X-RateLimit, etc.
+}
+
+// Example minimal schema validation for the 200 response.
+// In a real test suite, you would validate against the actual
+// #/components/schemas/ListEnvironmentVariablesResponse schema.
+function validateListEnvironmentVariablesResponse(data: any) {
+ // Example check: data might be an array of environment variables, or an object containing them
+ // Adjust these checks to match your actual schema.
+ expect(data).toBeDefined();
+ // If the response is expected to have a property like "environmentVariables" that is an array:
+ // expect(Array.isArray(data.environmentVariables)).toBe(true);
+ // For now, just check if data is an object or array.
+ expect(typeof data === 'object' || Array.isArray(data)).toBeTruthy();
+}
+
+// Example minimal schema validation for an ErrorResponse.
+function validateErrorResponse(data: any) {
+ // Adjust according to your actual #/components/schemas/ErrorResponse schema.
+ expect(data).toHaveProperty('error');
+ expect(typeof data.error).toBe('string');
+}
+
+describe('GET /api/v1/projects/{projectRef}/envvars/{env} - List environment variables', () => {
+ it('should return 200 and a valid response for valid path parameters', async () => {
+ const client = createApiClient(API_AUTH_TOKEN);
+
+ const response = await client.get(
+ `/api/v1/projects/${VALID_PROJECT_REF}/envvars/${VALID_ENV}`
+ );
+
+ // Expect 200 OK
+ expect(response.status).toBe(200);
+
+ // Validate headers
+ validateResponseHeaders(response.headers);
+
+ // Validate response body schema
+ validateListEnvironmentVariablesResponse(response.data);
+ });
+
+ it('should return 401 or 403 when no auth token is provided', async () => {
+ const client = createApiClient();
+
+ const response = await client.get(
+ `/api/v1/projects/${VALID_PROJECT_REF}/envvars/${VALID_ENV}`
+ );
+
+ // Expect unauthorized or forbidden
+ expect([401, 403]).toContain(response.status);
+
+ // When unauthorized or forbidden, we expect an error response body
+ validateErrorResponse(response.data);
+ });
+
+ it('should return 401 or 403 when an invalid auth token is provided', async () => {
+ const client = createApiClient('invalid_token');
+
+ const response = await client.get(
+ `/api/v1/projects/${VALID_PROJECT_REF}/envvars/${VALID_ENV}`
+ );
+
+ // Expect unauthorized or forbidden
+ expect([401, 403]).toContain(response.status);
+
+ // Validate error response body
+ validateErrorResponse(response.data);
+ });
+
+ it('should return 400 or 422 if path parameters are invalid format', async () => {
+ // Example: an empty projectRef or an obviously invalid env name
+ const client = createApiClient(API_AUTH_TOKEN);
+
+ const response1 = await client.get(
+ `/api/v1/projects/${INVALID_PROJECT_REF}/envvars/${VALID_ENV}`
+ );
+ // The API might return 400 or 422 for invalid input
+ expect([400, 422]).toContain(response1.status);
+ validateErrorResponse(response1.data);
+
+ const response2 = await client.get(
+ `/api/v1/projects/${VALID_PROJECT_REF}/envvars/${INVALID_ENV}`
+ );
+ // The API might return 400 or 422 for invalid input
+ expect([400, 422]).toContain(response2.status);
+ validateErrorResponse(response2.data);
+ });
+
+ it('should return 404 if the projectRef or env does not exist', async () => {
+ const client = createApiClient(API_AUTH_TOKEN);
+
+ const response1 = await client.get(
+ `/api/v1/projects/${NON_EXISTENT_PROJECT_REF}/envvars/${VALID_ENV}`
+ );
+ // Expect 404 when projectRef is not found
+ expect(response1.status).toBe(404);
+ validateErrorResponse(response1.data);
+
+ const response2 = await client.get(
+ `/api/v1/projects/${VALID_PROJECT_REF}/envvars/${NON_EXISTENT_ENV}`
+ );
+ // Expect 404 when environment is not found
+ expect(response2.status).toBe(404);
+ validateErrorResponse(response2.data);
+ });
+
+ it('should handle requests that might produce an empty list gracefully (if applicable)', async () => {
+ // In case the environment is valid but has no environment variables.
+ // Adjust if your API returns 200 with an empty array or a special response.
+
+ const client = createApiClient(API_AUTH_TOKEN);
+
+ // This test assumes that "emptyEnv" is a valid environment with no variables.
+ // You can adjust the environment name or the projectRef to produce an empty list.
+ const response = await client.get(
+ `/api/v1/projects/${VALID_PROJECT_REF}/envvars/emptyEnv`
+ );
+
+ // Even if no variables exist, it should still be a 200, returning an empty list.
+ // Or if your API returns 404 if no env vars exist, adjust accordingly.
+ expect([200, 404]).toContain(response.status);
+
+ if (response.status === 200) {
+ validateResponseHeaders(response.headers);
+ // Validate schema (likely an empty array or an object with empty array)
+ validateListEnvironmentVariablesResponse(response.data);
+ // Additional check if it returns an empty array
+ // expect(response.data.environmentVariables).toHaveLength(0);
+ } else {
+ // 404 scenario
+ validateErrorResponse(response.data);
+ }
+ });
+
+ it('should handle unexpected server error gracefully (500)', async () => {
+ // In many cases, forcing a 500 error can be challenging. This test scenario might be
+ // more hypothetical and depends on how your server triggers 500 errors.
+ // You might need to mock or simulate a server condition that returns 500.
+
+ // For demonstration, assume that using a special projectRef triggers a 500 in your test environment.
+ const projectRefCausingServerError = 'trigger500';
+ const client = createApiClient(API_AUTH_TOKEN);
+
+ const response = await client.get(
+ `/api/v1/projects/${projectRefCausingServerError}/envvars/${VALID_ENV}`
+ );
+
+ // Expect 500 or some other server error code
+ if (response.status >= 500 && response.status < 600) {
+ // Expecting server error responses
+ expect(true).toBe(true);
+ } else {
+ // If your API does not actually return 500 in test, just log it.
+ console.warn('Server did not produce a 500 error as expected.');
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.ts
new file mode 100644
index 0000000000..3ca2f5028d
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-projects-{projectRef}-runs.ts
@@ -0,0 +1,133 @@
+import axios, { AxiosError } from 'axios';
+
+describe('GET /api/v1/projects/{projectRef}/runs', () => {
+ const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ const token = process.env.API_AUTH_TOKEN || '';
+ const validProjectRef = 'my-valid-project';
+ const invalidProjectRef = '!@#'; // some invalid reference
+ const path = '/api/v1/projects';
+
+ beforeAll(() => {
+ if (!baseURL) {
+ console.warn('API_BASE_URL is not set. Tests may fail.');
+ }
+ });
+
+ describe('Input Validation', () => {
+ it('should return 200 for valid query parameters', async () => {
+ const url = `${baseURL}${path}/${validProjectRef}/runs?status=completed&page=1&limit=5`;
+ try {
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ expect(response.status).toBe(200);
+ // Additional assertions about response structure could go here
+ } catch (error) {
+ // If there's an error, force the test to fail
+ throw new Error(`Unexpected error: ${error}`);
+ }
+ });
+
+ it('should return 400 or 422 for invalid query parameter types', async () => {
+ const url = `${baseURL}${path}/${validProjectRef}/runs?limit=abc`; // limit is invalid type
+ try {
+ await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ fail('Expected request to fail');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([400, 422]).toContain(axiosError.response?.status);
+ }
+ });
+ });
+
+ describe('Response Validation', () => {
+ it('should return valid JSON and status 200 for a successful request', async () => {
+ const url = `${baseURL}${path}/${validProjectRef}/runs`;
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ // Here we can do partial schema validation:
+ expect(response.data).toHaveProperty('runs');
+ expect(Array.isArray(response.data.runs)).toBe(true);
+ });
+
+ it('should handle invalid projectRef gracefully', async () => {
+ const url = `${baseURL}${path}/${invalidProjectRef}/runs`;
+ try {
+ await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ fail('Expected request to fail due to invalid projectRef');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ // The API might return 400 or 404 if the projectRef is not valid
+ expect([400, 404]).toContain(axiosError.response?.status);
+ }
+ });
+ });
+
+ describe('Response Headers Validation', () => {
+ it('should have correct Content-Type header', async () => {
+ const url = `${baseURL}${path}/${validProjectRef}/runs`;
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ it('should return empty array if no runs are found', async () => {
+ const emptyProjectRef = 'project-with-no-runs';
+ const url = `${baseURL}${path}/${emptyProjectRef}/runs`;
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ expect(response.status).toBe(200);
+ expect(Array.isArray(response.data.runs)).toBe(true);
+ expect(response.data.runs.length).toBe(0);
+ });
+
+ it('should return 401 or 403 if token is missing', async () => {
+ const url = `${baseURL}${path}/${validProjectRef}/runs`;
+ try {
+ await axios.get(url); // no authorization header
+ fail('Expected request to fail due to missing token');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError.response?.status);
+ }
+ });
+
+ it('should return 401 or 403 if token is invalid', async () => {
+ const url = `${baseURL}${path}/${validProjectRef}/runs`;
+ try {
+ await axios.get(url, {
+ headers: {
+ Authorization: 'Bearer invalid-token',
+ },
+ });
+ fail('Expected request to fail due to invalid token');
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ expect([401, 403]).toContain(axiosError.response?.status);
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.ts
new file mode 100644
index 0000000000..59a78798f6
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-runs.ts
@@ -0,0 +1,192 @@
+import axios, { AxiosInstance, AxiosResponse } from 'axios';
+import { config as loadEnv } from 'dotenv';
+import { describe, beforeAll, afterAll, it, expect } from '@jest/globals';
+
+// Load environment variables
+loadEnv();
+
+// Create an axios instance for our tests
+let apiClient: AxiosInstance;
+
+beforeAll(() => {
+ apiClient = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+});
+
+afterAll(() => {
+ // Any cleanup logic can go here
+});
+
+describe('GET /api/v1/runs - List runs', () => {
+ it('should return 200 and a valid response body for a valid request with default parameters', async () => {
+ const response: AxiosResponse = await apiClient.get('/api/v1/runs');
+
+ // Response status
+ expect(response.status).toBe(200);
+
+ // Response headers
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Basic body validation (assuming the response returns an object with "runs")
+ expect(response.data).toHaveProperty('runs');
+ // Further validation can be performed if the OpenAPI schema is available.
+ });
+
+ it('should handle valid pagination query parameters (cursorPagination) and return 200', async () => {
+ // Example: Using page/limit or "cursor" style pagination if applicable
+ const params = {
+ limit: 5,
+ page: 1,
+ };
+
+ const response: AxiosResponse = await apiClient.get('/api/v1/runs', {
+ params,
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toHaveProperty('runs');
+ // Optionally check if returned "runs" length is <= limit
+ });
+
+ it('should filter runs by valid filter parameters (runsFilter) and return 200', async () => {
+ // Example status: "completed", version: "1.0.0"
+ const params = {
+ status: 'completed',
+ version: '1.0.0',
+ };
+
+ const response: AxiosResponse = await apiClient.get('/api/v1/runs', {
+ params,
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toHaveProperty('runs');
+ // Validate if the returned data actually matches the filter criteria if test data is known
+ });
+
+ it('should return an empty list if no matches are found for a given filter', async () => {
+ const params = {
+ status: 'nonexistent-status',
+ };
+
+ const response: AxiosResponse = await apiClient.get('/api/v1/runs', {
+ params,
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toHaveProperty('runs');
+ // Expect possibly an empty array
+ expect(Array.isArray(response.data.runs)).toBe(true);
+ // If the system returns an empty array for no matches:
+ expect(response.data.runs.length).toBe(0);
+ });
+
+ it('should return 400 or 422 for invalid query parameter types', async () => {
+ try {
+ // Passing a string where a number is expected, e.g., limit = 'invalid'
+ await apiClient.get('/api/v1/runs', {
+ params: {
+ limit: 'invalid',
+ },
+ });
+ // If it doesn’t throw, force fail
+ fail('Expected an error for invalid query parameters.');
+ } catch (error: any) {
+ // The API may return 400 or 422 in this scenario
+ const status = error.response?.status;
+ expect([400, 422]).toContain(status);
+ }
+ });
+
+ it('should return 401 or 403 when authorization token is invalid', async () => {
+ const invalidClient = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ headers: {
+ Authorization: 'Bearer invalid_token',
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ try {
+ await invalidClient.get('/api/v1/runs');
+ fail('Expected an unauthorized or forbidden error.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([401, 403]).toContain(status);
+ }
+ });
+
+ it('should return 401 or 403 when authorization header is missing', async () => {
+ const noAuthClient = axios.create({
+ baseURL: process.env.API_BASE_URL,
+ });
+
+ try {
+ await noAuthClient.get('/api/v1/runs');
+ fail('Expected an unauthorized or forbidden error.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([401, 403]).toContain(status);
+ }
+ });
+
+ it('should return 400 if request includes malformed query parameter', async () => {
+ try {
+ await apiClient.get('/api/v1/runs', {
+ params: {
+ status: '', // Possibly an empty string if status must be non-empty
+ },
+ });
+ // If no error, force fail
+ fail('Expected a 400 or 422 error for malformed query parameter.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([400, 422]).toContain(status);
+ }
+ });
+
+ it('should return 404 for a non-existing endpoint', async () => {
+ try {
+ await apiClient.get('/api/v1/runs-nonexisting');
+ fail('Expected a 404 Not Found error.');
+ } catch (error: any) {
+ // Some APIs might return 404 or 400, but typically 404 is expected for a missing route
+ expect(error.response?.status).toBe(404);
+ }
+ });
+
+ it('should handle server errors (5xx) gracefully if they occur', async () => {
+ // This test simulates or checks the API’s behavior for server-side errors.
+ // Without a real way to force a 5xx error, we typically rely on error conditions in local/dev environment.
+ // You might skip this test or simulate a scenario if your test environment can trigger a server error.
+ // Example is shown here for completeness:
+
+ try {
+ // Attempt a request that might trigger a server-side error
+ await apiClient.get('/api/v1/runs', {
+ params: {
+ causeServerError: true, // If the API has some debug flag (this is hypothetical)
+ },
+ });
+ fail('Expected a 5xx server error.');
+ } catch (error: any) {
+ const status = error.response?.status;
+ // Commonly, status would be 500 or maybe 503
+ if (status) {
+ expect(status).toBeGreaterThanOrEqual(500);
+ expect(status).toBeLessThan(600);
+ } else {
+ // If no status is returned, we fail the test
+ fail('Expected a 5xx server error, but none was received.');
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.ts
new file mode 100644
index 0000000000..db396011b2
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules-{schedule_id}.ts
@@ -0,0 +1,186 @@
+import axios, { AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
+
+/**
+ * Jest test suite for GET /api/v1/schedules/{schedule_id}
+ *
+ * Requirements Covered:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Authorization & Authentication
+ *
+ * Notes:
+ * - The base URL is loaded from the environment variable: API_BASE_URL
+ * - The auth token is loaded from the environment variable: API_AUTH_TOKEN
+ * - Since GET endpoints typically do not have a request body, payload-related tests here focus on path parameters.
+ * - For invalid/missing parameters, the API may return 400 or 422. Both are acceptable.
+ * - For unauthorized/forbidden requests, the API may return 401 or 403. Both are acceptable.
+ */
+
+describe('GET /api/v1/schedules/{schedule_id}', () => {
+ let baseURL: string;
+ let validToken: string;
+ let axiosInstance = axios.create();
+
+ beforeAll(() => {
+ baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ validToken = process.env.API_AUTH_TOKEN || '';
+ });
+
+ afterAll(() => {
+ // Clean up or tear down if needed
+ });
+
+ /**
+ * Helper function to perform GET request.
+ */
+ const getSchedule = async (
+ scheduleId: string | number | undefined,
+ token: string | undefined
+ ): Promise> => {
+ // Construct URL. If scheduleId is missing or invalid, that tests error behaviors.
+ const url = scheduleId
+ ? `${baseURL}/api/v1/schedules/${scheduleId}`
+ : `${baseURL}/api/v1/schedules/`; // Intentionally missing ID
+
+ return axiosInstance.get(url, {
+ headers: token
+ ? {
+ Authorization: `Bearer ${token}`,
+ }
+ : {},
+ });
+ };
+
+ describe('1. Input Validation', () => {
+ it('should return 400 or 422 if schedule_id is missing', async () => {
+ try {
+ await getSchedule(undefined, validToken);
+ // If the request does not fail, force a failure.
+ fail('Expected an error for missing schedule_id, but request succeeded.');
+ } catch (error: any) {
+ expect([400, 422, 404]).toContain(error?.response?.status);
+ // Depending on implementation, 404 might also be returned.
+ }
+ });
+
+ it('should return 400 or 422 if schedule_id is invalid (wrong type)', async () => {
+ // Passing a number where string is expected, for instance
+ try {
+ await getSchedule(12345, validToken);
+ fail('Expected an error for invalid schedule_id, but request succeeded.');
+ } catch (error: any) {
+ expect([400, 422, 404]).toContain(error?.response?.status);
+ }
+ });
+
+ it('should handle empty string as schedule_id', async () => {
+ try {
+ await getSchedule('', validToken);
+ fail('Expected an error for empty schedule_id, but request succeeded.');
+ } catch (error: any) {
+ expect([400, 422, 404]).toContain(error?.response?.status);
+ }
+ });
+ });
+
+ describe('2. Response Validation', () => {
+ it('should retrieve a schedule (200) with a valid schedule_id', async () => {
+ // Example known valid schedule ID
+ const scheduleId = 'sched_1234';
+
+ const response = await getSchedule(scheduleId, validToken);
+ expect(response.status).toBe(200);
+ // Check response body structure — assuming at least it has an "id" field
+ expect(response.data).toBeDefined();
+ expect(typeof response.data).toBe('object');
+ expect(response.data).toHaveProperty('id', scheduleId);
+ });
+
+ it('should return 404 when the schedule_id is not found', async () => {
+ const nonExistentId = 'sched_does_not_exist';
+ try {
+ await getSchedule(nonExistentId, validToken);
+ fail('Expected a 404 error for non-existent schedule, but request succeeded.');
+ } catch (error: any) {
+ // 404 expected for resource not found
+ expect(error?.response?.status).toBe(404);
+ }
+ });
+ });
+
+ describe('3. Response Headers Validation', () => {
+ it('should include Content-Type: application/json for a valid request', async () => {
+ const scheduleId = 'sched_1234';
+ const response = await getSchedule(scheduleId, validToken);
+
+ expect(response.headers).toBeDefined();
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+ });
+
+ describe('4. Edge Case & Limit Testing', () => {
+ it('should handle extremely large schedule_id gracefully (likely 400, 422, or 404)', async () => {
+ const largeScheduleId = 'sched_' + 'x'.repeat(1000); // very large ID
+ try {
+ await getSchedule(largeScheduleId, validToken);
+ fail('Expected an error for extremely large schedule_id, but request succeeded.');
+ } catch (error: any) {
+ // Depending on implementation details, it might return 400, 422, or 404.
+ expect([400, 422, 404]).toContain(error?.response?.status);
+ }
+ });
+
+ // GET requests typically do not return empty arrays unless the resource is a collection
+ // but we check if 404 is returned instead of an empty response if the ID is not found.
+ it('should return 404 instead of an empty object/array if the schedule is not found', async () => {
+ const nonExistentId = 'sched_nonexistent';
+ try {
+ await getSchedule(nonExistentId, validToken);
+ fail('Expected a 404 for non-existent schedule, got success.');
+ } catch (error: any) {
+ expect(error?.response?.status).toBe(404);
+ }
+ });
+
+ it('should handle server error (5xx) gracefully if it occurs', async () => {
+ // This test is conceptual; if the server is not mocked to produce 5xx,
+ // you can catch the scenario if any unhandled error occurs.
+ // We'll simulate by using an unrealistic endpoint.
+ try {
+ await axiosInstance.get(`${baseURL}/api/v1/schedules/trigger-500-error`);
+ // If no error, we can skip.
+ } catch (error: any) {
+ // If a 500 occurs, test is satisfied.
+ if (error?.response?.status === 500) {
+ expect(error?.response?.status).toBe(500);
+ }
+ }
+ });
+ });
+
+ describe('5. Testing Authorization & Authentication', () => {
+ it('should return 401 or 403 if the request is made without a token', async () => {
+ const scheduleId = 'sched_1234';
+ try {
+ await getSchedule(scheduleId, undefined);
+ fail('Expected a 401/403 error for missing token, but request succeeded.');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error?.response?.status);
+ }
+ });
+
+ it('should return 401 or 403 if the token is invalid', async () => {
+ const scheduleId = 'sched_1234';
+ const invalidToken = 'invalid_token';
+ try {
+ await getSchedule(scheduleId, invalidToken);
+ fail('Expected a 401/403 error for invalid token, but request succeeded.');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error?.response?.status);
+ }
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.ts
new file mode 100644
index 0000000000..08bf3d7cb1
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-schedules.ts
@@ -0,0 +1,216 @@
+import axios, { AxiosError, AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/*************************************************
+ * Test Suite for GET /api/v1/schedules
+ * Method: GET
+ * Path: /api/v1/schedules
+ * Description: List all schedules with optional pagination.
+ *************************************************/
+
+describe('GET /api/v1/schedules', () => {
+ let baseURL: string;
+ let token: string;
+
+ beforeAll(() => {
+ // Load environment variables
+ baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ token = process.env.API_AUTH_TOKEN || '';
+ });
+
+ /**
+ * Helper function to create an axios instance with common headers
+ */
+ const getAxiosInstance = (authToken?: string) => {
+ return axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
+ },
+ validateStatus: () => true, // We handle status checks manually
+ });
+ };
+
+ /**
+ * 1. Input Validation
+ * - Test valid/invalid query params.
+ */
+ it('should return 200 OK with valid query parameters (page, perPage)', async () => {
+ const instance = getAxiosInstance(token);
+
+ const response: AxiosResponse = await instance.get('/api/v1/schedules', {
+ params: {
+ page: 1,
+ perPage: 10,
+ },
+ });
+
+ // Expect a 200 status for valid query params
+ expect(response.status).toBe(200);
+ // Check for application/json header
+ expect(response.headers['content-type']).toContain('application/json');
+ // Check for a valid response body structure (partial, as example)
+ expect(response.data).toBeDefined();
+ // Example: If the schema includes a schedules array, verify
+ // Adjust the property checks below to match the actual schema from #/components/schemas/ListSchedulesResult
+ // expect(Array.isArray(response.data.schedules)).toBe(true);
+ });
+
+ it('should allow no query parameters and return 200 with a default page result', async () => {
+ const instance = getAxiosInstance(token);
+
+ const response: AxiosResponse = await instance.get('/api/v1/schedules');
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ expect(response.data).toBeDefined();
+ // Perform partial schema checks
+ });
+
+ it('should return 400 or 422 when page parameter is invalid (e.g., a string)', async () => {
+ const instance = getAxiosInstance(token);
+
+ const response: AxiosResponse = await instance.get('/api/v1/schedules', {
+ params: {
+ page: 'invalid',
+ },
+ });
+
+ // The API may return 400 or 422 for invalid parameters.
+ expect([400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should return 400 or 422 when perPage parameter is invalid (e.g., negative)', async () => {
+ const instance = getAxiosInstance(token);
+
+ const response: AxiosResponse = await instance.get('/api/v1/schedules', {
+ params: {
+ perPage: -10,
+ },
+ });
+
+ expect([400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ /**
+ * 2. Response Validation
+ * - Validate 200 success structure, error codes, etc.
+ */
+ it('should match the expected 200 response schema for a valid request', async () => {
+ const instance = getAxiosInstance(token);
+ const response: AxiosResponse = await instance.get('/api/v1/schedules');
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Example partial schema validation
+ // Adjust to match #/components/schemas/ListSchedulesResult
+ // expect(Array.isArray(response.data.schedules)).toBe(true);
+ // expect(response.data.page).toBeDefined();
+ // expect(response.data.total).toBeDefined();
+ });
+
+ /**
+ * 3. Response Headers Validation
+ * - Check "Content-Type", etc.
+ */
+ it('should include Content-Type header in the response', async () => {
+ const instance = getAxiosInstance(token);
+
+ const response: AxiosResponse = await instance.get('/api/v1/schedules');
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ /**
+ * 4. Edge Case & Limit Testing
+ */
+ it('should return an empty array or valid structure if no schedules exist (edge case)', async () => {
+ // This test presumes a scenario where the database might be empty.
+ // If your environment always has schedules, adapt as needed.
+ const instance = getAxiosInstance(token);
+
+ const response: AxiosResponse = await instance.get('/api/v1/schedules', {
+ params: {
+ page: 999999, // Large page number may return an empty list
+ perPage: 100,
+ },
+ });
+
+ // Expect a successful response with potential empty data.
+ expect(response.status).toBe(200);
+ expect(response.data).toBeDefined();
+ // Adjust property checks based on your actual response schema.
+ // Example:
+ // expect(Array.isArray(response.data.schedules)).toBe(true);
+ // expect(response.data.schedules.length).toBe(0);
+ });
+
+ it('should ensure proper handling with extremely large perPage value', async () => {
+ const instance = getAxiosInstance(token);
+
+ const response: AxiosResponse = await instance.get('/api/v1/schedules', {
+ params: {
+ perPage: 999999999, // A large integer to test boundaries.
+ },
+ });
+
+ // The API might still return 200 or possibly 400 if it's out of range.
+ // Adjust expectations based on your API's behavior.
+ expect([200, 400, 422]).toContain(response.status);
+ });
+
+ it('should handle server errors gracefully if the server returns a 500 (hypothetical test)', async () => {
+ // This test assumes you might force a 500 error by some specific input or environment.
+ // Adjust or remove this if 500 is not easily triggered.
+ const instance = getAxiosInstance(token);
+
+ // Example only: Not guaranteed to trigger a 500.
+ // In a real environment, you might have a special test setup to provoke a server error.
+ try {
+ const response = await instance.get('/api/v1/schedules', {
+ params: {
+ page: -999999, // Possibly invalid enough to cause a server error in some implementations.
+ },
+ });
+ // If the server does not return 500, check acceptable alternate statuses.
+ expect([200, 400, 422]).toContain(response.status);
+ } catch (err) {
+ const error = err as AxiosError;
+ // Check if we indeed got a 500
+ if (error.response) {
+ expect(error.response.status).toBe(500);
+ }
+ }
+ });
+
+ /**
+ * 5. Testing Authorization & Authentication
+ * - Test valid, invalid, and missing credentials.
+ */
+ it('should return 200 with a valid token', async () => {
+ // Assuming token is valid if provided.
+ if (!token) {
+ console.warn('No valid API_AUTH_TOKEN found; skipping test.');
+ return;
+ }
+
+ const instance = getAxiosInstance(token);
+ const response: AxiosResponse = await instance.get('/api/v1/schedules');
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should return 401 or 403 for missing or invalid token', async () => {
+ const instance = getAxiosInstance('InvalidOrMissingToken');
+ const response: AxiosResponse = await instance.get('/api/v1/schedules');
+
+ // The API might return 401 or 403.
+ expect([401, 403]).toContain(response.status);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.ts b/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.ts
new file mode 100644
index 0000000000..14e21f0aa0
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v1-timezones.ts
@@ -0,0 +1 @@
+import axios from 'axios';\n\ndescribe('GET /api/v1/timezones', () => {\n const baseURL = process.env.API_BASE_URL || '';\n const authToken = process.env.API_AUTH_TOKEN || '';\n let axiosInstance;\n\n beforeAll(() => {\n axiosInstance = axios.create({\n baseURL,\n validateStatus: () => true, // allow non-2xx responses so we can test error conditions\n headers: {\n // Use template literals for authorization header\n Authorization: `Bearer ${authToken}`\n }\n });\n });\n\n it('should return 200 and valid JSON when excludeUtc is not provided', async () => {\n const response = await axiosInstance.get('/api/v1/timezones');\n expect(response.status).toBe(200);\n // Response Headers Validation\n expect(response.headers['content-type']).toContain('application/json');\n // Response Body Validation\n expect(response.data).toBeDefined();\n // Assuming the response schema includes an array property named timezones\n expect(Array.isArray(response.data.timezones)).toBe(true);\n });\n\n it('should return 200 with excludeUtc=true', async () => {\n const response = await axiosInstance.get('/api/v1/timezones?excludeUtc=true');\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n expect(Array.isArray(response.data.timezones)).toBe(true);\n // Additional checks to confirm UTC is excluded if needed.\n });\n\n it('should return 200 with excludeUtc=false', async () => {\n const response = await axiosInstance.get('/api/v1/timezones?excludeUtc=false');\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n expect(Array.isArray(response.data.timezones)).toBe(true);\n // Additional checks to confirm UTC is included if needed.\n });\n\n it('should return 400 or 422 if excludeUtc is invalid type', async () => {\n // Providing an invalid string in place of a boolean should yield 400 or 422.\n const response = await axiosInstance.get('/api/v1/timezones?excludeUtc=abc');\n expect([400, 422]).toContain(response.status);\n });\n\n it('should return 401 or 403 if no auth token is provided', async () => {\n const noAuthInstance = axios.create({\n baseURL,\n validateStatus: () => true\n });\n const response = await noAuthInstance.get('/api/v1/timezones');\n expect([401, 403]).toContain(response.status);\n });\n\n it('should return 401 or 403 if invalid auth token is provided', async () => {\n const invalidAuthInstance = axios.create({\n baseURL,\n validateStatus: () => true,\n headers: {\n Authorization: 'Bearer invalidTokenHere'\n }\n });\n const response = await invalidAuthInstance.get('/api/v1/timezones');\n expect([401, 403]).toContain(response.status);\n });\n});\n
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.ts b/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.ts
new file mode 100644
index 0000000000..3215566fb1
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_get_api-v3-runs-{runId}.ts
@@ -0,0 +1,184 @@
+import axios, { AxiosInstance } from 'axios';
+import { AxiosResponse } from 'axios';
+
+/**
+ * Jest test suite for GET /api/v3/runs/{runId}
+ *
+ * Environment variables:
+ * - API_BASE_URL: Base URL of the API (e.g. https://api.example.com)
+ * - API_AUTH_TOKEN: Authentication token (can be public or secret key)
+ * - API_PUBLIC_TOKEN: (Optional) Another token to test public-key behavior.
+ *
+ * Note: This test suite demonstrates various scenarios for input validation,
+ * response validation, response headers, edge cases, and authentication.
+ * Replace the placeholder run IDs with real or mock values for your actual testing.
+ */
+
+describe('GET /api/v3/runs/{runId}', () => {
+ let client: AxiosInstance;
+
+ const baseURL = process.env.API_BASE_URL;
+ const secretToken = process.env.API_AUTH_TOKEN; // Presumed secret key
+ const publicToken = process.env.API_PUBLIC_TOKEN; // Optionally used for public-key tests
+
+ // Placeholder run IDs. Replace these with actual/valid IDs for integration tests.
+ const validRunId = 'valid-run-id';
+ const nonExistentRunId = 'non-existent-run-id';
+ const invalidRunId = '!!!'; // Malformed run ID
+
+ beforeAll(() => {
+ // Create an axios instance with baseURL
+ client = axios.create({
+ baseURL,
+ timeout: 15000, // 15 seconds
+ validateStatus: () => true, // Let us handle the status code checks in tests.
+ });
+ });
+
+ /**
+ * Helper to make the GET request.
+ * @param runId The run ID to retrieve.
+ * @param token The authorization token.
+ */
+ const getRun = async (runId: string, token?: string): Promise => {
+ const headers: Record = {};
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ return client.get(`/api/v3/runs/${runId}`, {
+ headers,
+ });
+ };
+
+ /********************************************************************************
+ * 1. INPUT VALIDATION TESTS
+ ********************************************************************************/
+
+ it('should return 400 or 422 for invalid run ID format', async () => {
+ // For an obviously invalid runId, the API may respond with 400 or 422.
+ const response = await getRun(invalidRunId, secretToken);
+
+ // Check that the status is either 400 or 422
+ expect([400, 422]).toContain(response.status);
+
+ // Optionally check the error message structure
+ if (response.status === 400 || response.status === 422) {
+ expect(response.data).toHaveProperty('error');
+ }
+ });
+
+ it('should return 404 if run does not exist', async () => {
+ // The API may respond with 404 if the run is not found.
+ const response = await getRun(nonExistentRunId, secretToken);
+
+ expect(response.status).toBe(404);
+ expect(response.data).toHaveProperty('error', 'Run not found');
+ });
+
+ /********************************************************************************
+ * 2. RESPONSE VALIDATION
+ ********************************************************************************/
+
+ it('should return 200 and match the expected schema for a valid run ID (secret token)', async () => {
+ // Assuming the validRunId refers to an existing run.
+ const response = await getRun(validRunId, secretToken);
+
+ expect(response.status).toBe(200);
+ // Check for presence of required fields in the response.
+ // The actual schema keys may differ based on your OpenAPI definitions.
+ // For example:
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('status');
+ expect(response.data).toHaveProperty('payload');
+ expect(response.data).toHaveProperty('output');
+ expect(response.data).toHaveProperty('attempts');
+
+ // Additional checks for field types, etc.
+ // e.g., expect(typeof response.data.status).toBe('string');
+ });
+
+ /********************************************************************************
+ * 3. RESPONSE HEADERS VALIDATION
+ ********************************************************************************/
+
+ it('should include appropriate response headers for a valid run', async () => {
+ const response = await getRun(validRunId, secretToken);
+
+ // Verify status code.
+ expect(response.status).toBe(200);
+
+ // Check Content-Type header is application/json.
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Check for other optional headers like Cache-Control or Rate-Limit.
+ // Example:
+ // expect(response.headers).toHaveProperty('cache-control');
+ // expect(response.headers).toHaveProperty('x-ratelimit-limit');
+ });
+
+ /********************************************************************************
+ * 4. EDGE CASE & LIMIT TESTING
+ ********************************************************************************/
+
+ it('should return 401 or 403 if no auth token is provided', async () => {
+ // Missing token scenario.
+ const response = await getRun(validRunId);
+
+ // The API might return 401 or 403.
+ expect([401, 403]).toContain(response.status);
+
+ // Optional: Check error message.
+ if (response.status === 401) {
+ expect(response.data).toHaveProperty('error', 'Invalid or Missing API key');
+ } else if (response.status === 403) {
+ // Some APIs might differentiate.
+ // Check the error structure if relevant.
+ }
+ });
+
+ it('should handle extremely large or malformed run ID gracefully', async () => {
+ const largeRunId = 'a'.repeat(1000); // A very long run ID.
+ const response = await getRun(largeRunId, secretToken);
+
+ // Expecting a client or server validation error.
+ expect([400, 422, 404]).toContain(response.status);
+ });
+
+ // This test simulates checking no results found scenario, though for GET by ID,
+ // a non-existent run might be the typical scenario. Already tested with 404.
+
+ /********************************************************************************
+ * 5. AUTHENTICATION & AUTHORIZATION TESTS
+ ********************************************************************************/
+
+ it('should omit payload and output when using a public token (if applicable)', async () => {
+ // Only run this test if a public token is defined in environment.
+ if (!publicToken) {
+ console.warn('No public token found. Skipping public-key test.');
+ return;
+ }
+
+ const response = await getRun(validRunId, publicToken);
+
+ // Expect success with status 200 if the run ID is valid.
+ // But the payload and output should be omitted.
+ expect(response.status).toBe(200);
+
+ // Expect the presence of other fields.
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('status');
+
+ // The "payload" and "output" fields should be omitted for public key requests.
+ expect(response.data).not.toHaveProperty('payload');
+ expect(response.data).not.toHaveProperty('output');
+ });
+
+ it('should return 401 or 403 for an invalid or expired token', async () => {
+ const invalidToken = 'Bearer invalid-or-expired-token';
+ const response = await getRun(validRunId, invalidToken);
+
+ // Expect either 401 or 403.
+ expect([401, 403]).toContain(response.status);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.ts
new file mode 100644
index 0000000000..2b2ec03d91
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}-import.ts
@@ -0,0 +1,226 @@
+```typescript
+import axios, { AxiosInstance, AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/************************************************************
+ * Jest test suite for:
+ * POST /api/v1/projects/{projectRef}/envvars/{env}/import
+ *
+ * This test suite covers:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Testing Authorization & Authentication
+ ************************************************************/
+
+describe('POST /api/v1/projects/{projectRef}/envvars/{env}/import', () => {
+ let client: AxiosInstance;
+ let validProjectRef = 'example-project-123';
+ let validEnv = 'development';
+
+ beforeAll(() => {
+ const baseURL = process.env.API_BASE_URL || 'http://localhost:3000';
+ const token = process.env.API_AUTH_TOKEN || '';
+
+ client = axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ validateStatus: () => true, // Allow handling status codes in tests
+ });
+ });
+
+ /************************************************************
+ * 1) Successful upload of environment variables (200 OK)
+ ************************************************************/
+ it('should upload environment variables successfully (200)', async () => {
+ // Example of a valid request body based on an assumed schema:
+ // {
+ // vars: [
+ // { key: 'VAR_KEY', value: 'VAR_VALUE' },
+ // ...
+ // ]
+ // }
+ const validRequestBody = {
+ vars: [
+ { key: 'TEST_KEY', value: 'TEST_VALUE' },
+ { key: 'ANOTHER_KEY', value: 'ANOTHER_VALUE' },
+ ],
+ };
+
+ const response: AxiosResponse = await client.post(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/import`,
+ validRequestBody
+ );
+
+ // Response Validation
+ expect([200]).toContain(response.status);
+ expect(response.data).toBeDefined();
+ // Example: Check if response follows success structure
+ // Adjust property checks based on your actual schema
+ // For instance, if SucceedResponse has a "message" field:
+ // expect(response.data).toHaveProperty('message');
+
+ // Response Headers Validation
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ });
+
+ /************************************************************
+ * 2) Invalid request body -> expect 400 or 422
+ ************************************************************/
+ it('should return 400 or 422 for invalid request body', async () => {
+ const invalidRequestBody = {
+ // Missing or malformed "vars" field
+ // e.g., string instead of array
+ vars: "this-should-be-an-array",
+ };
+
+ const response: AxiosResponse = await client.post(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/import`,
+ invalidRequestBody
+ );
+
+ // Expecting 400 or 422 for invalid payload
+ expect([400, 422]).toContain(response.status);
+ expect(response.data).toBeDefined();
+
+ // Check if error response structure matches expectations (e.g., error details)
+ // Example:
+ // expect(response.data).toHaveProperty('error');
+
+ // Response Headers Validation
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ });
+
+ /************************************************************
+ * 3) Unauthorized or forbidden -> expect 401 or 403
+ ************************************************************/
+ it('should return 401 or 403 when the Authorization token is missing or invalid', async () => {
+ // Create a client without auth token
+ const unauthorizedClient = axios.create({
+ baseURL: process.env.API_BASE_URL || 'http://localhost:3000',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ validateStatus: () => true,
+ });
+
+ const validRequestBody = {
+ vars: [{ key: 'KEY_NO_AUTH', value: 'VALUE_NO_AUTH' }],
+ };
+
+ const response: AxiosResponse = await unauthorizedClient.post(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/import`,
+ validRequestBody
+ );
+
+ // Expecting 401 or 403
+ expect([401, 403]).toContain(response.status);
+ // Check if response content matches expected structure
+ // For example:
+ // expect(response.data).toMatchObject({ error: expect.any(String) });
+ });
+
+ /************************************************************
+ * 4) Resource not found -> expect 404
+ ************************************************************/
+ it('should return 404 when projectRef or env does not exist', async () => {
+ const invalidProjectRef = 'non-existing-project';
+ const invalidEnv = 'non-existing-env';
+
+ const validRequestBody = {
+ vars: [{ key: 'TEST_KEY_404', value: 'TEST_VALUE_404' }],
+ };
+
+ const response: AxiosResponse = await client.post(
+ `/api/v1/projects/${invalidProjectRef}/envvars/${invalidEnv}/import`,
+ validRequestBody
+ );
+
+ // Expecting 404
+ expect(response.status).toBe(404);
+ // Check response structure if applicable
+ // Example:
+ // expect(response.data).toHaveProperty('error');
+
+ // Response Headers Validation
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ });
+
+ /************************************************************
+ * 5) Edge case: Empty request body
+ * - Depending on API constraints, expect 400, 422, or success if empty is allowed
+ ************************************************************/
+ it('should handle empty request body', async () => {
+ const emptyRequestBody = {};
+
+ const response: AxiosResponse = await client.post(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/import`,
+ emptyRequestBody
+ );
+
+ // Expecting 400 or 422 if empty requests are invalid
+ // or possibly 200 if the API allows an empty import
+ expect([200, 400, 422]).toContain(response.status);
+
+ // If success is valid for empty imports, we can check success structure;
+ // otherwise, check error.
+ // expect(response.data).toHaveProperty('error'); or similar.
+
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ });
+
+ /************************************************************
+ * 6) Edge case: Large payload
+ * - Test the API handling of large imports.
+ ************************************************************/
+ it('should handle a large payload of environment variables', async () => {
+ // Creating a large array of environment variables
+ const largeVars = Array.from({ length: 1000 }, (_, index) => ({
+ key: `LARGE_KEY_${index}`,
+ value: `LARGE_VALUE_${index}`,
+ }));
+
+ const largeRequestBody = {
+ vars: largeVars,
+ };
+
+ const response: AxiosResponse = await client.post(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/import`,
+ largeRequestBody
+ );
+
+ // Expect success or appropriate handling (e.g., 413 if the payload is too large)
+ expect([200, 400, 413, 422]).toContain(response.status);
+
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ });
+
+ /************************************************************
+ * 7) Malformed request (simulate server error handling)
+ * - If relevant, we can test for 500 or other 5xx.
+ ************************************************************/
+ it('should handle server errors (simulate a malformed request that leads to 500)', async () => {
+ // This test depends on whether the server can produce a 500
+ // For demonstration, we pass an obviously incorrect structure.
+
+ const malformedBody = {
+ vars: 12345, // Not an array or object structure as expected
+ };
+
+ const response: AxiosResponse = await client.post(
+ `/api/v1/projects/${validProjectRef}/envvars/${validEnv}/import`,
+ malformedBody
+ );
+
+ // Some servers may return 400 or 422 instead of 500 for malformed bodies.
+ // If your server can return 500, adjust the test accordingly.
+ expect([400, 422, 500]).toContain(response.status);
+
+ expect(response.headers['content-type']).toMatch(/application\/json/);
+ });
+});
+```
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.ts
new file mode 100644
index 0000000000..2bca33a8a2
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-projects-{projectRef}-envvars-{env}.ts
@@ -0,0 +1,212 @@
+import axios, { AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+// Load environment variables
+const API_BASE_URL = process.env.API_BASE_URL;
+const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN;
+
+// Common test data
+const VALID_PROJECT_REF = 'test-project';
+const VALID_ENV = 'production';
+
+// Construct endpoint
+// Example: https://example.com/api/v1/projects/test-project/envvars/production
+function getEndpoint(projectRef: string, env: string): string {
+ return `${API_BASE_URL}/api/v1/projects/${projectRef}/envvars/${env}`;
+}
+
+// Valid body payload based on presumed schema for creating an environment variable.
+// Adjust fields as needed to match your actual API schema.
+const validRequestBody = {
+ name: 'MY_VARIABLE',
+ value: 'someValue',
+};
+
+// Helper function to make requests
+async function makeRequest(
+ projectRef: string,
+ env: string,
+ data: any,
+ token: string | undefined = API_AUTH_TOKEN
+): Promise {
+ const url = getEndpoint(projectRef, env);
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ }
+
+ return axios.post(url, data, { headers });
+}
+
+describe('POST /api/v1/projects/{projectRef}/envvars/{env}', () => {
+ beforeAll(() => {
+ if (!API_BASE_URL) {
+ throw new Error('API_BASE_URL environment variable is not defined.');
+ }
+ });
+
+ describe('Input Validation', () => {
+ it('should create environment variable with valid data (200 response)', async () => {
+ const response = await makeRequest(VALID_PROJECT_REF, VALID_ENV, validRequestBody);
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ // Check if response body matches expected schema (e.g., has "success" or similar)
+ // Adjust this validation to fit the actual "SucceedResponse" schema.
+ expect(response.data).toHaveProperty('success');
+ expect(typeof response.data.success).toBe('boolean');
+ });
+
+ it('should return 400 or 422 when required fields are missing', async () => {
+ // Missing "name" and "value"
+ const invalidBody = {};
+ try {
+ await makeRequest(VALID_PROJECT_REF, VALID_ENV, invalidBody);
+ } catch (error: any) {
+ const status = error.response?.status;
+ // Either 400 or 422 is acceptable for invalid payload
+ expect([400, 422]).toContain(status);
+ expect(error.response.data).toBeDefined();
+ }
+ });
+
+ it('should return 400 or 422 when fields have wrong types', async () => {
+ // Provide number instead of string
+ const invalidBody = {
+ name: 1234,
+ value: 5678,
+ };
+ try {
+ await makeRequest(VALID_PROJECT_REF, VALID_ENV, invalidBody);
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([400, 422]).toContain(status);
+ expect(error.response.data).toBeDefined();
+ }
+ });
+
+ it('should handle empty string as a field value and potentially return 400 or 422', async () => {
+ const invalidBody = {
+ name: '',
+ value: ''
+ };
+ try {
+ await makeRequest(VALID_PROJECT_REF, VALID_ENV, invalidBody);
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([400, 422]).toContain(status);
+ expect(error.response.data).toBeDefined();
+ }
+ });
+ });
+
+ describe('Response Validation', () => {
+ it('should return the correct success structure for valid inputs', async () => {
+ const response = await makeRequest(VALID_PROJECT_REF, VALID_ENV, validRequestBody);
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+ // Validate response body structure against the expected schema
+ expect(response.data).toHaveProperty('success');
+ expect(typeof response.data.success).toBe('boolean');
+ });
+
+ it('should return 404 if projectRef or environment does not exist', async () => {
+ const nonExistentProjectRef = 'non-existent-project';
+
+ try {
+ await makeRequest(nonExistentProjectRef, VALID_ENV, validRequestBody);
+ } catch (error: any) {
+ expect([404]).toContain(error.response?.status);
+ expect(error.response.data).toBeDefined();
+ }
+ });
+
+ // Note: The API might return 500 for server errors, or some other code.
+ // This is a placeholder test in case the server triggers a 5xx.
+ it('should handle unexpected server errors gracefully (simulate 500)', async () => {
+ // Simulation approach: if the API doesn’t let you force a 500 easily,
+ // you might skip or externally test this scenario.
+ // For demonstration, we assume an invalid environment name triggers a 500 in some rare scenario.
+ const invalidEnv = 'simulate-500';
+ try {
+ await makeRequest(VALID_PROJECT_REF, invalidEnv, validRequestBody);
+ } catch (error: any) {
+ // You might replace this logic depending on how your API surfaces errors.
+ expect([500]).toContain(error.response?.status);
+ }
+ });
+ });
+
+ describe('Response Headers Validation', () => {
+ it('should include application/json in Content-Type for successful request', async () => {
+ const response = await makeRequest(VALID_PROJECT_REF, VALID_ENV, validRequestBody);
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ // Add more header checks as needed, e.g. X-RateLimit, Cache-Control, etc.
+ it('should include standard headers (e.g., Cache-Control) if applicable', async () => {
+ const response = await makeRequest(VALID_PROJECT_REF, VALID_ENV, validRequestBody);
+ // Example check:
+ // expect(response.headers['cache-control']).toBeDefined();
+ // Adjust based on your API’s actual headers.
+ expect(response.status).toBe(200);
+ });
+ });
+
+ describe('Edge Case & Limit Testing', () => {
+ it('should handle extremely large payload (potentially 413 or 400)', async () => {
+ // Create a large string
+ const largeString = 'x'.repeat(100000); // 100k characters
+ const largePayload = {
+ name: largeString,
+ value: largeString,
+ };
+
+ try {
+ await makeRequest(VALID_PROJECT_REF, VALID_ENV, largePayload);
+ } catch (error: any) {
+ // Depending on how the server handles large payloads:
+ // could be 413 Payload Too Large, 400, or 422
+ expect([400, 413, 422]).toContain(error.response?.status);
+ }
+ });
+
+ it('should return proper response when payload is empty', async () => {
+ try {
+ await makeRequest(VALID_PROJECT_REF, VALID_ENV, null);
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response?.status);
+ }
+ });
+ });
+
+ describe('Testing Authorization & Authentication', () => {
+ it('should return 401 or 403 when no auth token is provided', async () => {
+ try {
+ await makeRequest(VALID_PROJECT_REF, VALID_ENV, validRequestBody, undefined /* no token */);
+ } catch (error: any) {
+ const status = error.response?.status;
+ // The API could return either 401 or 403.
+ expect([401, 403]).toContain(status);
+ }
+ });
+
+ it('should return 401 or 403 when auth token is invalid', async () => {
+ try {
+ await makeRequest(VALID_PROJECT_REF, VALID_ENV, validRequestBody, 'invalid-token');
+ } catch (error: any) {
+ const status = error.response?.status;
+ expect([401, 403]).toContain(status);
+ }
+ });
+
+ it('should succeed (200) with a valid auth token', async () => {
+ const response = await makeRequest(VALID_PROJECT_REF, VALID_ENV, validRequestBody, API_AUTH_TOKEN);
+ expect(response.status).toBe(200);
+ expect(response.data).toHaveProperty('success');
+ });
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.ts
new file mode 100644
index 0000000000..9d87336a35
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-replay.ts
@@ -0,0 +1,168 @@
+import axios, { AxiosResponse } from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+// These environment variables should be set in your test environment.
+// Example:
+// API_BASE_URL=https://your-api-endpoint.com
+// API_AUTH_TOKEN=someValidAuthToken
+const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
+const AUTH_TOKEN = process.env.API_AUTH_TOKEN || '';
+
+// Helper function to create configured axios instance.
+// We disable axios' default status throwing so we can test response codes explicitly.
+function createAxiosInstance(token?: string) {
+ return axios.create({
+ baseURL: BASE_URL,
+ validateStatus: () => true, // Let us handle response codes manually
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: token ? `Bearer ${token}` : '',
+ },
+ });
+}
+
+/**
+ * Comprehensive Jest test suite for POST /api/v1/runs/{runId}/replay
+ *
+ * This covers:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Testing Authorization & Authentication
+ */
+describe('POST /api/v1/runs/{runId}/replay', () => {
+ // A known valid runId for testing (replace with a real one if available).
+ // In a real-world setting, you might first create a run, then replay it.
+ const validRunId = 'existing-run-id';
+
+ // A runId that is presumably not found in the system.
+ const nonExistentRunId = 'non-existent-run-id';
+
+ // A runId that is invalid (e.g., empty), expected to cause error 400 or 422.
+ const invalidRunId = '';
+
+ // A runId that is malformed or extremely large.
+ const largeRunId = 'x'.repeat(1024); // 1024 characters
+
+ it('should replay a run successfully with a valid runId (expect 200)', async () => {
+ const axiosInstance = createAxiosInstance(AUTH_TOKEN);
+ const response: AxiosResponse = await axiosInstance.post(
+ `/api/v1/runs/${validRunId}/replay`
+ );
+
+ // Check status code
+ expect(response.status).toBe(200);
+
+ // Check response headers
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Check response body schema
+ expect(response.data).toHaveProperty('id');
+ expect(typeof response.data.id).toBe('string');
+ });
+
+ it('should return 400 or 422 for an invalid or empty runId', async () => {
+ const axiosInstance = createAxiosInstance(AUTH_TOKEN);
+ const response: AxiosResponse = await axiosInstance.post(
+ `/api/v1/runs/${invalidRunId}/replay`
+ );
+
+ // Expect 400 or 422
+ expect([400, 422]).toContain(response.status);
+
+ // Check response headers
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Check error structure
+ expect(response.data).toHaveProperty('error');
+ // The error might be one of:
+ // - "Invalid or missing run ID"
+ // - "Failed to create new run"
+ // or another validation message if 422 is used.
+ });
+
+ it('should return 404 if the runId does not exist', async () => {
+ const axiosInstance = createAxiosInstance(AUTH_TOKEN);
+ const response: AxiosResponse = await axiosInstance.post(
+ `/api/v1/runs/${nonExistentRunId}/replay`
+ );
+
+ // Expect 404
+ expect(response.status).toBe(404);
+
+ // Check response headers
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ expect(response.data).toHaveProperty('error');
+ // Should be "Run not found" as per schema
+ expect(response.data.error).toBe('Run not found');
+ });
+
+ it('should handle extremely large runId (expect 400 or 422)', async () => {
+ const axiosInstance = createAxiosInstance(AUTH_TOKEN);
+ const response: AxiosResponse = await axiosInstance.post(
+ `/api/v1/runs/${largeRunId}/replay`
+ );
+
+ // We expect the server to reject this with 400 or 422.
+ expect([400, 422]).toContain(response.status);
+
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ expect(response.data).toHaveProperty('error');
+ });
+
+ it('should return 401 or 403 when no auth token is provided', async () => {
+ const axiosInstance = createAxiosInstance(); // No token
+ const response: AxiosResponse = await axiosInstance.post(
+ `/api/v1/runs/${validRunId}/replay`
+ );
+
+ // Expect 401 or 403 for missing or invalid token
+ expect([401, 403]).toContain(response.status);
+
+ // Check response headers
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Check error structure
+ expect(response.data).toHaveProperty('error');
+ // The error might be "Invalid or Missing API key" or another unauthorized/forbidden error.
+ });
+
+ it('should return 401 or 403 for an invalid auth token', async () => {
+ const axiosInstance = createAxiosInstance('invalid-token');
+ const response: AxiosResponse = await axiosInstance.post(
+ `/api/v1/runs/${validRunId}/replay`
+ );
+
+ expect([401, 403]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ expect(response.data).toHaveProperty('error');
+ });
+
+ it('should handle potential server error (5xx) gracefully', async () => {
+ // Forcing a 500 can be tricky, but we can show a test skeleton.
+ // In practice, you might set up a scenario that triggers a server error.
+
+ const axiosInstance = createAxiosInstance(AUTH_TOKEN);
+
+ // This is just a demonstration. Adjust if you have a known condition that triggers 500.
+ // For example, you might pass some parameter that the server is known to handle incorrectly.
+ const response: AxiosResponse = await axiosInstance.post(
+ `/api/v1/runs/${validRunId}/replay`,
+ {
+ // Possibly a known invalid or conflicting payload if the API expects or allows a body.
+ }
+ );
+
+ // If the server truly returns 500, you can test it like:
+ if (response.status >= 500 && response.status < 600) {
+ expect(response.status).toBeGreaterThanOrEqual(500);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ expect(response.data).toHaveProperty('error');
+ } else {
+ // If no 5xx is returned, at least check that we did not succeed.
+ expect(response.status).not.toBe(200);
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.ts
new file mode 100644
index 0000000000..47bc9ee7ab
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-runs-{runId}-reschedule.ts
@@ -0,0 +1,214 @@
+import axios, { AxiosInstance, AxiosResponse } from "axios";
+import { describe, it, beforeAll, expect } from "@jest/globals";
+
+/**
+ * Test file for POST /api/v1/runs/{runId}/reschedule endpoint
+ * Framework: Jest
+ * Language: TypeScript
+ * HTTP Client: Axios
+ *
+ * Make sure to set the following environment variables:
+ * - API_BASE_URL (e.g., https://api.example.com)
+ * - API_AUTH_TOKEN (your valid API token)
+ */
+
+const baseURL = process.env.API_BASE_URL || "http://localhost:3000";
+const validToken = process.env.API_AUTH_TOKEN || "";
+
+// Helper function to create an Axios instance with or without auth
+function createAxiosInstance(useAuth = true): AxiosInstance {
+ const headers: Record = {};
+ if (useAuth) {
+ headers["Authorization"] = `Bearer ${validToken}`;
+ }
+
+ return axios.create({
+ baseURL,
+ headers,
+ });
+}
+
+// A valid run ID for successful testing (assumes a run in the DELAYED state).
+// Update "validRunId" and "delayedRunId" with appropriate test values.
+const validRunId = "123";
+// Invalid run IDs to test error handling
+const invalidRunId = "abc";
+const nonExistentRunId = "9999999"; // ID that presumably does not exist
+
+// Sample body that might match the expected request schema.
+// Adjust field names/types based on actual OpenAPI schema.
+interface RescheduleRunRequest {
+ // Example field: the new delay (in seconds) for the delayed run
+ delayInSeconds: number;
+}
+
+const validRequestBody: RescheduleRunRequest = {
+ delayInSeconds: 300, // 5 minutes
+};
+
+// Some variants for edge cases
+const zeroDelayRequestBody: RescheduleRunRequest = {
+ delayInSeconds: 0,
+};
+
+const largeDelayRequestBody: RescheduleRunRequest = {
+ delayInSeconds: 999999999, // Arbitrarily large number
+};
+
+const invalidRequestBodyType: any = {
+ delayInSeconds: "not-a-number", // Wrong data type
+};
+
+// Utility to check common headers
+function expectCommonHeaders(response: AxiosResponse) {
+ // Content-Type should be application/json on success or error
+ expect(response.headers["content-type"]).toMatch(/application\/json/i);
+ // You can add more header checks here, e.g., Cache-Control
+ // expect(response.headers["cache-control"]).toBeDefined();
+}
+
+describe("POST /api/v1/runs/{runId}/reschedule", () => {
+ let client: AxiosInstance;
+
+ beforeAll(() => {
+ client = createAxiosInstance();
+ });
+
+ /**
+ * 1. Input Validation Tests
+ */
+ describe("Input Validation", () => {
+ it("should return 400 or 422 when runId is invalid", async () => {
+ expect.assertions(2);
+ try {
+ await client.post(`/api/v1/runs/${invalidRunId}/reschedule`, validRequestBody);
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ expectCommonHeaders(error.response);
+ }
+ });
+
+ it("should return 400 or 422 when request body has invalid data type", async () => {
+ expect.assertions(2);
+ try {
+ await client.post(`/api/v1/runs/${validRunId}/reschedule`, invalidRequestBodyType);
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ expectCommonHeaders(error.response);
+ }
+ });
+
+ it("should return 400 or 422 when required body is missing", async () => {
+ expect.assertions(2);
+ try {
+ // Sending undefined or empty body
+ await client.post(`/api/v1/runs/${validRunId}/reschedule`, {});
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ expectCommonHeaders(error.response);
+ }
+ });
+ });
+
+ /**
+ * 2. Response Validation Tests
+ */
+ describe("Response Validation", () => {
+ it("should return 200 and match the schema on valid input", async () => {
+ const response = await client.post(
+ `/api/v1/runs/${validRunId}/reschedule`,
+ validRequestBody
+ );
+
+ expect(response.status).toBe(200);
+ expectCommonHeaders(response);
+
+ // Example schema checks (the actual schema depends on RetrieveRunResponse)
+ // For demonstration, we assume it might have an 'id' (string) and 'status' (string).
+ // Adjust field checks to match your actual schema.
+ const data = response.data as {
+ id?: string;
+ status?: string;
+ [key: string]: unknown;
+ };
+
+ expect(data).toBeDefined();
+ expect(typeof data.id).toBe("string");
+ expect(typeof data.status).toBe("string");
+ });
+
+ it("should return 404 if run does not exist", async () => {
+ expect.assertions(2);
+ try {
+ await client.post(`/api/v1/runs/${nonExistentRunId}/reschedule`, validRequestBody);
+ } catch (error: any) {
+ expect(error.response.status).toBe(404);
+ expectCommonHeaders(error.response);
+ }
+ });
+ });
+
+ /**
+ * 3. Response Headers Validation
+ */
+ describe("Response Headers Validation", () => {
+ it("should include correct headers on success", async () => {
+ const response = await client.post(
+ `/api/v1/runs/${validRunId}/reschedule`,
+ validRequestBody
+ );
+
+ expect(response.status).toBe(200);
+ expect(response.headers["content-type"]).toMatch(/application\/json/i);
+ });
+ });
+
+ /**
+ * 4. Edge Case & Limit Testing
+ */
+ describe("Edge Case & Limit Testing", () => {
+ it("should handle zero delay gracefully", async () => {
+ // 0 might be a boundary value for the delay
+ const response = await client.post(
+ `/api/v1/runs/${validRunId}/reschedule`,
+ zeroDelayRequestBody
+ );
+ expect(response.status).toBe(200);
+ expectCommonHeaders(response);
+ });
+
+ it("should handle large delay values", async () => {
+ const response = await client.post(
+ `/api/v1/runs/${validRunId}/reschedule`,
+ largeDelayRequestBody
+ );
+ expect(response.status).toBe(200);
+ expectCommonHeaders(response);
+ });
+
+ it("should return 401 or 403 if request is unauthorized", async () => {
+ expect.assertions(2);
+ try {
+ const unauthClient = createAxiosInstance(false);
+ await unauthClient.post(`/api/v1/runs/${validRunId}/reschedule`, validRequestBody);
+ } catch (error: any) {
+ expect([401, 403]).toContain(error.response.status);
+ expectCommonHeaders(error.response);
+ }
+ });
+
+ it("should handle server errors (simulated test)", async () => {
+ /**
+ * This is a placeholder example. If you have a way to trigger 500 errors (or other 5xx errors),
+ * you can do so here. Otherwise, you might mock or simulate it.
+ */
+ expect(true).toBe(true);
+ });
+ });
+
+ /**
+ * 5. Testing Authorization & Authentication
+ * Covered in the unauthorized test above. Additional tests can be added
+ * if there are multiple roles or permission levels.
+ */
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.ts
new file mode 100644
index 0000000000..2e9e0ec875
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-activate.ts
@@ -0,0 +1,176 @@
+import axios, { AxiosError } from 'axios';
+
+describe('POST /api/v1/schedules/{schedule_id}/activate', () => {
+ let baseUrl: string;
+ let token: string;
+ // Replace with real IDs if available/testable in your environment.
+ let validScheduleId = 'valid_schedule_id';
+ let nonImperativeScheduleId = 'non_imperative_schedule_id';
+ let nonExistentScheduleId = 'non_existent_schedule_id';
+ let largeScheduleId = 'x'.repeat(1000); // 1000 characters
+
+ beforeAll(() => {
+ // Load env vars for base URL and auth token.
+ baseUrl = process.env.API_BASE_URL || '';
+ token = process.env.API_AUTH_TOKEN || '';
+ });
+
+ const getHeaders = (authToken: string | null = token) => {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+ if (authToken) {
+ headers['Authorization'] = `Bearer ${authToken}`;
+ }
+ return headers;
+ };
+
+ it('should activate the schedule with valid ID and return 200', async () => {
+ expect(baseUrl).toBeTruthy();
+ expect(token).toBeTruthy();
+
+ const url = `${baseUrl}/api/v1/schedules/${validScheduleId}/activate`;
+
+ const response = await axios.post(url, {}, {
+ headers: getHeaders()
+ });
+
+ // Response Validation
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Optionally validate other headers
+ // expect(response.headers['cache-control']).toBeDefined();
+ // expect(response.headers['x-ratelimit']).toBeDefined();
+
+ // Response body schema validation (partial example)
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('status');
+ // Add further field/type checks as needed.
+ });
+
+ it('should return 401 or 403 when token is invalid', async () => {
+ expect(baseUrl).toBeTruthy();
+
+ const url = `${baseUrl}/api/v1/schedules/${validScheduleId}/activate`;
+
+ try {
+ await axios.post(url, {}, {
+ headers: getHeaders('invalid_token')
+ });
+ fail('Request should have failed with 401 or 403');
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ expect([401, 403]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 401 or 403 when token is missing', async () => {
+ expect(baseUrl).toBeTruthy();
+
+ const url = `${baseUrl}/api/v1/schedules/${validScheduleId}/activate`;
+
+ try {
+ await axios.post(url, {}, {
+ headers: getHeaders(null)
+ });
+ fail('Request should have failed with 401 or 403');
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ expect([401, 403]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 400 or 422 if schedule_id is empty', async () => {
+ expect(baseUrl).toBeTruthy();
+ expect(token).toBeTruthy();
+
+ // Intentionally leave schedule_id empty.
+ const url = `${baseUrl}/api/v1/schedules//activate`;
+
+ try {
+ await axios.post(url, {}, {
+ headers: getHeaders()
+ });
+ fail('Request should have failed with 400 or 422');
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 400 or 422 if schedule_id is extremely large', async () => {
+ expect(baseUrl).toBeTruthy();
+ expect(token).toBeTruthy();
+
+ const url = `${baseUrl}/api/v1/schedules/${largeScheduleId}/activate`;
+
+ try {
+ await axios.post(url, {}, {
+ headers: getHeaders()
+ });
+ fail('Request should have failed with 400 or 422');
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 404 if the schedule_id does not exist', async () => {
+ expect(baseUrl).toBeTruthy();
+ expect(token).toBeTruthy();
+
+ const url = `${baseUrl}/api/v1/schedules/${nonExistentScheduleId}/activate`;
+
+ try {
+ await axios.post(url, {}, {
+ headers: getHeaders()
+ });
+ fail('Request should have failed with 404');
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ expect(error.response.status).toBe(404);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should return 400 or 422 if the schedule is not IMPERATIVE', async () => {
+ expect(baseUrl).toBeTruthy();
+ expect(token).toBeTruthy();
+
+ const url = `${baseUrl}/api/v1/schedules/${nonImperativeScheduleId}/activate`;
+
+ try {
+ await axios.post(url, {}, {
+ headers: getHeaders()
+ });
+ fail('Request should have failed with 400 or 422');
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.ts
new file mode 100644
index 0000000000..4c3e7c5e4c
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules-{schedule_id}-deactivate.ts
@@ -0,0 +1,184 @@
+import axios from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/**
+ * Jest test suite for the POST /api/v1/schedules/:schedule_id/deactivate endpoint.
+ * This suite covers:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Authorization & Authentication Testing
+ *
+ * Prerequisites:
+ * - Set environment variables API_BASE_URL and API_AUTH_TOKEN.
+ * - The API might return 400 or 422 for invalid inputs.
+ * - The API might return 401 or 403 for unauthorized or forbidden requests.
+ */
+
+describe('POST /api/v1/schedules/:schedule_id/deactivate', () => {
+ let baseUrl: string;
+ let token: string;
+
+ beforeAll(() => {
+ baseUrl = process.env.API_BASE_URL || '';
+ token = process.env.API_AUTH_TOKEN || '';
+ });
+
+ it('should deactivate a schedule successfully with a valid schedule_id (expect 200)', async () => {
+ // Replace with a known-valid schedule ID
+ const validScheduleId = 'someExistingImperativeScheduleId';
+ let response: any;
+
+ try {
+ response = await axios.post(
+ `${baseUrl}/api/v1/schedules/${validScheduleId}/deactivate`,
+ null,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + token
+ }
+ }
+ );
+ } catch (error: any) {
+ // If we catch an error, the test fails
+ expect(error).toBeFalsy();
+ }
+
+ expect(response).toBeDefined();
+ expect(response.status).toBe(200);
+
+ // Basic response body validation
+ expect(response.data).toBeDefined();
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('name');
+ expect(response.data).toHaveProperty('status');
+
+ // Response header validation
+ expect(response.headers['content-type']).toContain('application/json');
+ });
+
+ it('should return 401 or 403 for an invalid or missing auth token', async () => {
+ // Replace with a known-valid schedule ID
+ const validScheduleId = 'someExistingImperativeScheduleId';
+ let error: any;
+
+ try {
+ // Provide an invalid token
+ await axios.post(
+ `${baseUrl}/api/v1/schedules/${validScheduleId}/deactivate`,
+ null,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer invalid_or_missing_token'
+ }
+ }
+ );
+ } catch (err: any) {
+ error = err;
+ }
+
+ expect(error).toBeDefined();
+ // Could be 401 or 403
+ expect([401, 403]).toContain(error?.response?.status);
+ });
+
+ it('should return 404 if the schedule is not found', async () => {
+ const notFoundScheduleId = 'thisScheduleDoesNotExist';
+ let error: any;
+
+ try {
+ await axios.post(
+ `${baseUrl}/api/v1/schedules/${notFoundScheduleId}/deactivate`,
+ null,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + token
+ }
+ }
+ );
+ } catch (err: any) {
+ error = err;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.response?.status).toBe(404);
+ });
+
+ it('should return 400 or 422 when schedule_id is invalid (e.g. empty string)', async () => {
+ // Using whitespace or empty string to simulate invalid input
+ const invalidScheduleId = ' ';
+ let error: any;
+
+ try {
+ await axios.post(
+ `${baseUrl}/api/v1/schedules/${invalidScheduleId}/deactivate`,
+ null,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + token
+ }
+ }
+ );
+ } catch (err: any) {
+ error = err;
+ }
+
+ expect(error).toBeDefined();
+ // Could be 400 or 422
+ expect([400, 422]).toContain(error?.response?.status);
+ });
+
+ it('should handle a large schedule_id (likely 400, 422, or 404)', async () => {
+ // Very long fake schedule_id
+ const largeScheduleId = 'a'.repeat(256);
+ let error: any;
+
+ try {
+ await axios.post(
+ `${baseUrl}/api/v1/schedules/${largeScheduleId}/deactivate`,
+ null,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + token
+ }
+ }
+ );
+ } catch (err: any) {
+ error = err;
+ }
+
+ expect(error).toBeDefined();
+ // Could be 400, 422, or 404 depending on server-side validation
+ expect([400, 422, 404]).toContain(error?.response?.status);
+ });
+
+ it('should return 401 or 403 if the user is not authenticated at all', async () => {
+ // Attempt request without any Authorization header
+ const validScheduleId = 'someExistingImperativeScheduleId';
+ let error: any;
+
+ try {
+ await axios.post(
+ `${baseUrl}/api/v1/schedules/${validScheduleId}/deactivate`,
+ null,
+ {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+ } catch (err: any) {
+ error = err;
+ }
+
+ expect(error).toBeDefined();
+ // Could be 401 or 403
+ expect([401, 403]).toContain(error?.response?.status);
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts
new file mode 100644
index 0000000000..61ee2427c9
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts
@@ -0,0 +1,148 @@
+import axios, { AxiosResponse } from 'axios';
+import { describe, it, expect } from '@jest/globals';
+
+/**
+ * Jest test suite for POST /api/v1/schedules.
+ * This suite covers:
+ * 1. Input Validation (required params, data types, edge cases)
+ * 2. Response Validation (status codes, schema, error handling)
+ * 3. Response Headers Validation (Content-Type, etc.)
+ * 4. Edge Case & Limit Testing (large payload, boundary values, invalid requests)
+ * 5. Testing Authorization & Authentication
+ */
+
+describe('POST /api/v1/schedules', () => {
+ const baseURL = process.env.API_BASE_URL;
+ const authToken = process.env.API_AUTH_TOKEN;
+
+ // Helper function to build the request config (including Authorization header)
+ const buildConfig = (token?: string) => {
+ return {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ };
+ };
+
+ // A valid payload conforming to the (hypothetical) required schema for creating an IMPERATIVE schedule.
+ // Adjust the fields to match your actual ScheduleObject schema.
+ const validPayload = {
+ name: 'My IMPERATIVE Schedule',
+ type: 'IMPERATIVE',
+ startDate: '2023-12-31T00:00:00Z',
+ endDate: '2024-01-07T00:00:00Z',
+ repeat: false,
+ };
+
+ // Utility for checking expected properties in the response body.
+ // Adjust to match your actual schema structure.
+ const validateScheduleObject = (data: any) => {
+ // Example checks based on hypothetical "ScheduleObject" schema:
+ expect(data).toHaveProperty('id');
+ expect(typeof data.id).toBe('string');
+ expect(data).toHaveProperty('name');
+ expect(typeof data.name).toBe('string');
+ expect(data).toHaveProperty('type');
+ expect(data.type).toBe('IMPERATIVE');
+ };
+
+ it('should create a new schedule with valid payload (200)', async () => {
+ expect(baseURL).toBeDefined();
+ expect(authToken).toBeDefined();
+
+ const url = `${baseURL}/api/v1/schedules`;
+
+ const response: AxiosResponse = await axios.post(url, validPayload, buildConfig(authToken));
+
+ // Response validation
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toContain('application/json');
+
+ // Validate the response body against the expected schema
+ validateScheduleObject(response.data);
+ });
+
+ it('should return 400 or 422 if required fields are missing', async () => {
+ const url = `${baseURL}/api/v1/schedules`;
+ // Remove a required field (e.g., "type") from the payload
+ const invalidPayload = { ...validPayload };
+ delete invalidPayload.type;
+
+ try {
+ await axios.post(url, invalidPayload, buildConfig(authToken));
+ // If we reach here, no error was thrown, which is unexpected
+ throw new Error('Expected request to fail with 400 or 422, but it succeeded.');
+ } catch (error: any) {
+ expect(error.response).toBeDefined();
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+
+ it('should return 400 or 422 if an invalid data type is provided', async () => {
+ const url = `${baseURL}/api/v1/schedules`;
+ // Provide an invalid "name" type (number instead of string)
+ const invalidPayload = { ...validPayload, name: 12345 };
+
+ try {
+ await axios.post(url, invalidPayload, buildConfig(authToken));
+ throw new Error('Expected request to fail with 400 or 422, but it succeeded.');
+ } catch (error: any) {
+ expect(error.response).toBeDefined();
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+
+ it('should return 401 or 403 if the request is unauthorized', async () => {
+ const url = `${baseURL}/api/v1/schedules`;
+
+ try {
+ // No auth token provided
+ await axios.post(url, validPayload, buildConfig());
+ throw new Error('Expected 401 or 403 for unauthorized request, but it succeeded.');
+ } catch (error: any) {
+ expect(error.response).toBeDefined();
+ expect([401, 403]).toContain(error.response.status);
+ }
+ });
+
+ it('should handle large payload or boundary cases gracefully (400 or 422)', async () => {
+ const url = `${baseURL}/api/v1/schedules`;
+ // Construct an extremely long string for name
+ const largeString = 'a'.repeat(2000); // Example boundary test
+ const boundaryPayload = { ...validPayload, name: largeString };
+
+ try {
+ await axios.post(url, boundaryPayload, buildConfig(authToken));
+ // Depending on API constraints, this may succeed or fail.
+ // If your schema disallows long strings, expect an error.
+ // Failing here simply ensures we handle whichever the spec dictates.
+ } catch (error: any) {
+ // If it fails, 400 or 422 is acceptable.
+ if (error.response) {
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ // If no response, it might be a server/network error.
+ throw error;
+ }
+ }
+ });
+
+ it('should return appropriate error code when sending empty request body (400 or 422)', async () => {
+ const url = `${baseURL}/api/v1/schedules`;
+
+ try {
+ await axios.post(url, {}, buildConfig(authToken));
+ throw new Error('Expected 400 or 422 for empty request body, but it succeeded.');
+ } catch (error: any) {
+ expect(error.response).toBeDefined();
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+
+ // Additional tests can be added for:
+ // - server errors (5xx) handling
+ // - rate limiting scenarios
+ // - forbidden (403) with valid token but insufficient permissions (if applicable)
+ // - etc.
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.ts
new file mode 100644
index 0000000000..2badc4aa7e
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-batch.ts
@@ -0,0 +1,130 @@
+import axios, { AxiosResponse, AxiosError } from 'axios';
+
+const API_BASE_URL = process.env.API_BASE_URL;
+const API_AUTH_TOKEN = process.env.API_AUTH_TOKEN;
+
+describe('POST /api/v1/tasks/batch', () => {
+ /**
+ * Helper function to create a valid tasks payload.
+ * @param count - Number of tasks to generate.
+ */
+ function createValidPayload(count = 1) {
+ const tasks = [];
+ for (let i = 0; i < count; i++) {
+ tasks.push({
+ taskId: `task-${i}`,
+ data: {
+ foo: `bar-${i}`
+ }
+ });
+ }
+ return { tasks };
+ }
+
+ /**
+ * Create a client instance with authentication.
+ * We set validateStatus to always return true,
+ * so we can handle the status codes directly in the tests.
+ */
+ const client = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ Authorization: `Bearer ${API_AUTH_TOKEN}`
+ },
+ validateStatus: () => true
+ });
+
+ it('should return 401 or 403 if auth token is missing', async () => {
+ // No Authorization header here
+ const response = await axios.post(`${API_BASE_URL}/api/v1/tasks/batch`, createValidPayload(), {
+ validateStatus: () => true
+ });
+
+ // Expecting 401 Unauthorized or 403 Forbidden
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should return 200 for a valid payload', async () => {
+ const response = await client.post('/api/v1/tasks/batch', createValidPayload());
+
+ // Expecting successful response
+ expect(response.status).toBe(200);
+
+ // Validate the response headers
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Validate the response body structure (basic check)
+ expect(response.data).toBeDefined();
+ // Additional checks can be performed here, e.g.:
+ // expect(response.data).toHaveProperty('results');
+ });
+
+ it('should return 400 or 422 for an empty payload', async () => {
+ const response = await client.post('/api/v1/tasks/batch', {});
+
+ // Expecting 400 Bad Request or 422 Unprocessable Entity
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should return 400 or 422 when tasks exceed 500', async () => {
+ // Create a payload with 501 tasks
+ const response = await client.post('/api/v1/tasks/batch', createValidPayload(501));
+
+ // Expecting 400 Bad Request or 422 Unprocessable Entity
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should return 400 or 422 for invalid data type in payload', async () => {
+ // Here the tasks property is a string instead of an array
+ const invalidPayload = {
+ tasks: 'invalid'
+ };
+
+ const response = await client.post('/api/v1/tasks/batch', invalidPayload);
+
+ // Expecting 400 Bad Request or 422 Unprocessable Entity
+ expect([400, 422]).toContain(response.status);
+ });
+
+ it('should return 404 for non-existing endpoint', async () => {
+ // Hitting a non-existing path to check for 404
+ const response = await client.post('/api/v1/nonexisting', createValidPayload());
+ expect(response.status).toBe(404);
+ });
+
+ it('should return 401 or 403 if auth token is invalid', async () => {
+ // Create a client with an invalid token
+ const clientWithInvalidToken = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ Authorization: 'Bearer invalid_token'
+ },
+ validateStatus: () => true
+ });
+
+ const response = await clientWithInvalidToken.post('/api/v1/tasks/batch', createValidPayload());
+
+ // Expecting 401 Unauthorized or 403 Forbidden
+ expect([401, 403]).toContain(response.status);
+ });
+
+ it('should handle an empty tasks array (0 tasks)', async () => {
+ // Depending on the API's design, 0 tasks might be valid or invalid
+ const payload = { tasks: [] };
+ const response = await client.post('/api/v1/tasks/batch', payload);
+
+ // The status could be 200 (if empty is valid) or 400/422 if not
+ expect([200, 400, 422]).toContain(response.status);
+ });
+
+ it('should include the correct response headers for valid requests', async () => {
+ const response = await client.post('/api/v1/tasks/batch', createValidPayload());
+
+ // Check the Content-Type header
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+
+ // Additional header checks can go here
+ // For example:
+ // expect(response.headers).toHaveProperty('cache-control');
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.ts b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.ts
new file mode 100644
index 0000000000..2e17df9423
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-tasks-{taskIdentifier}-trigger.ts
@@ -0,0 +1,124 @@
+import axios, { AxiosError } from 'axios';
+import { describe, test, expect, beforeAll } from '@jest/globals';
+
+const baseURL = process.env.API_BASE_URL || '';
+const validAuthToken = process.env.API_AUTH_TOKEN || '';
+
+function createAxiosInstance(token?: string) {
+ return axios.create({
+ baseURL,
+ headers: {
+ Authorization: token ? `Bearer ${token}` : '',
+ 'Content-Type': 'application/json',
+ },
+ });
+}
+
+describe('POST /api/v1/tasks/{taskIdentifier}/trigger', () => {
+ let axiosInstance = createAxiosInstance(validAuthToken);
+
+ beforeAll(() => {
+ // Recreate axios instance if needed, for example ensuring fresh tokens
+ axiosInstance = createAxiosInstance(validAuthToken);
+ });
+
+ test('should trigger a task successfully with valid data (200)', async () => {
+ const taskIdentifier = 'validTask123';
+
+ const response = await axiosInstance.post(`/api/v1/tasks/${taskIdentifier}/trigger`, {});
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ // Example response schema validation
+ expect(response.data).toHaveProperty('status');
+ expect(typeof response.data.status).toBe('string');
+ });
+
+ test('should return 401 or 403 for unauthorized requests when token is missing or invalid', async () => {
+ const noAuthInstance = createAxiosInstance();
+ const taskIdentifier = 'validTask123';
+
+ try {
+ await noAuthInstance.post(`/api/v1/tasks/${taskIdentifier}/trigger`, {});
+ fail('Request should not succeed without a valid token');
+ } catch (error) {
+ if (error instanceof AxiosError && error.response) {
+ // API might respond with 401 or 403 if unauthorized
+ expect([401, 403]).toContain(error.response.status);
+ expect(error.response.headers['content-type']).toMatch(/application\/json/i);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ test('should return 400 or 422 for invalid or malformed request data', async () => {
+ // For example, invalid or empty taskIdentifier
+ const invalidTaskIdentifier = '';
+
+ try {
+ await axiosInstance.post(`/api/v1/tasks/${invalidTaskIdentifier}/trigger`, { foo: 'bar' });
+ fail('Request should not succeed with an invalid path parameter');
+ } catch (error) {
+ if (error instanceof AxiosError && error.response) {
+ // Depending on implementation, API might return 400 or 422
+ expect([400, 422]).toContain(error.response.status);
+ expect(error.response.headers['content-type']).toMatch(/application\/json/i);
+ // Validate error response structure
+ expect(error.response.data).toHaveProperty('error');
+ expect(typeof error.response.data.error).toBe('string');
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ test('should return 404 if the task identifier does not exist', async () => {
+ const nonExistentTaskIdentifier = 'does-not-exist-000';
+
+ try {
+ await axiosInstance.post(`/api/v1/tasks/${nonExistentTaskIdentifier}/trigger`, {});
+ fail('Request should not succeed for a non-existent resource');
+ } catch (error) {
+ if (error instanceof AxiosError && error.response) {
+ expect(error.response.status).toBe(404);
+ expect(error.response.headers['content-type']).toMatch(/application\/json/i);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ test('should handle large payload gracefully', async () => {
+ const taskIdentifier = 'validTask123';
+ // Simulate a large request body
+ const largeData = 'x'.repeat(1000000);
+
+ const response = await axiosInstance.post(`/api/v1/tasks/${taskIdentifier}/trigger`, { testData: largeData });
+ // Depending on API limits, we might get 200, 400, or 413
+ expect([200, 400, 413]).toContain(response.status);
+ });
+
+ test('should include correct response headers when successful', async () => {
+ const taskIdentifier = 'validTask123';
+ const response = await axiosInstance.post(`/api/v1/tasks/${taskIdentifier}/trigger`, {});
+
+ expect(response.headers).toHaveProperty('content-type');
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ });
+
+ test('should handle server errors (5xx) gracefully (simulated)', async () => {
+ // This test assumes there is a way to simulate a server error by using a special identifier
+ try {
+ await axiosInstance.post('/api/v1/tasks/errorTrigger/trigger', {});
+ fail('Request should have triggered an error');
+ } catch (error) {
+ if (error instanceof AxiosError && error.response) {
+ // We expect certain 5xx codes here
+ expect([500, 503]).toContain(error.response.status);
+ } else {
+ throw error;
+ }
+ }
+ });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.ts b/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.ts
new file mode 100644
index 0000000000..0a743406ba
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_post_api-v2-runs-{runId}-cancel.ts
@@ -0,0 +1,126 @@
+import axios, { AxiosInstance } from 'axios';
+import { describe, expect, test, beforeAll } from '@jest/globals';
+
+describe('POST /api/v2/runs/:runId/cancel', () => {
+ let axiosInstance: AxiosInstance;
+ const validRunId = 'run_1234'; // Sample run ID for a valid scenario
+ const invalidRunId = 'invalid_run_id'; // Sample run ID for an invalid scenario
+ const nonExistentRunId = 'run_non_existent'; // Sample run ID that doesn't exist
+
+ beforeAll(() => {
+ // create an axios instance with baseURL and default headers
+ axiosInstance = axios.create({
+ baseURL: process.env.API_BASE_URL
+ });
+ });
+
+ test('Should cancel a run successfully (200) with a valid run ID', async () => {
+ try {
+ const response = await axiosInstance.post(`/api/v2/runs/${validRunId}/cancel`, {}, {
+ headers: {
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`
+ }
+ });
+
+ // Response status check
+ expect(response.status).toBe(200);
+ // Response headers check
+ expect(response.headers['content-type']).toContain('application/json');
+ // Response body schema check
+ expect(response.data).toHaveProperty('id');
+ expect(typeof response.data.id).toBe('string');
+ } catch (error: any) {
+ // If we get an error here, fail the test explicitly
+ throw new Error(`Expected 200, but received error: ${error.message}`);
+ }
+ });
+
+ test('Should return 400 or 422 when run ID is invalid', async () => {
+ expect.assertions(1);
+
+ try {
+ await axiosInstance.post(`/api/v2/runs/${invalidRunId}/cancel`, {}, {
+ headers: {
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`
+ }
+ });
+ } catch (error: any) {
+ if (error.response) {
+ // Input validation error codes
+ expect([400, 422]).toContain(error.response.status);
+ } else {
+ throw new Error(`Expected 400 or 422, but no valid response found. Error: ${error.message}`);
+ }
+ }
+ });
+
+ test('Should return 401 or 403 if the token is missing or invalid', async () => {
+ expect.assertions(1);
+
+ try {
+ // No Authorization header provided
+ await axiosInstance.post(`/api/v2/runs/${validRunId}/cancel`);
+ } catch (error: any) {
+ if (error.response) {
+ // Unauthorized or forbidden
+ expect([401, 403]).toContain(error.response.status);
+ } else {
+ throw new Error(`Expected 401 or 403, but no valid response found. Error: ${error.message}`);
+ }
+ }
+ });
+
+ test('Should return 404 if the run ID does not exist', async () => {
+ expect.assertions(2);
+
+ try {
+ await axiosInstance.post(`/api/v2/runs/${nonExistentRunId}/cancel`, {}, {
+ headers: {
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`
+ }
+ });
+ } catch (error: any) {
+ if (error.response) {
+ // Resource not found
+ expect(error.response.status).toBe(404);
+ expect(error.response.data).toHaveProperty('error', 'Run not found');
+ } else {
+ throw new Error(`Expected 404, but no valid response found. Error: ${error.message}`);
+ }
+ }
+ });
+
+ test('Should validate response headers (Content-Type is application/json)', async () => {
+ try {
+ const response = await axiosInstance.post(`/api/v2/runs/${validRunId}/cancel`, {}, {
+ headers: {
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`
+ }
+ });
+
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ } catch (error: any) {
+ throw new Error(`Error occurred: ${error.message}`);
+ }
+ });
+
+ test('Should handle server errors (500) gracefully if triggered', async () => {
+ expect.assertions(1);
+
+ // This test presumes there's a way to force a 500 from the server.
+ // For demonstration purposes, we'll just illustrate how the test would look.
+ try {
+ await axiosInstance.post('/api/v2/runs/trigger_500_error/cancel', {}, {
+ headers: {
+ Authorization: `Bearer ${process.env.API_AUTH_TOKEN}`
+ }
+ });
+ } catch (error: any) {
+ if (error.response) {
+ expect(error.response.status).toBe(500);
+ } else {
+ throw new Error(`Expected 500, but no valid response found. Error: ${error.message}`);
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts b/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
new file mode 100644
index 0000000000..fa71bee56b
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-projects-{projectRef}-envvars-{env}-{name}.ts
@@ -0,0 +1,255 @@
+import axios, { AxiosError, AxiosResponse } from 'axios';
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+/**
+ * Test suite for PUT /api/v1/projects/{projectRef}/envvars/{env}/{name}
+ *
+ * This suite covers:
+ * 1. Input Validation
+ * 2. Response Validation
+ * 3. Response Headers Validation
+ * 4. Edge Case & Limit Testing
+ * 5. Authorization & Authentication Testing
+ */
+
+describe('PUT /api/v1/projects/{projectRef}/envvars/{env}/{name}', () => {
+ let apiBaseUrl: string;
+ let authToken: string;
+
+ const validProjectRef = 'testProject';
+ const validEnv = 'development';
+ const validName = 'TEST_VAR';
+
+ /**
+ * Utility function to build the full endpoint URL.
+ */
+ const buildUrl = (
+ projectRef: string,
+ env: string,
+ name: string
+ ): string => {
+ return `${apiBaseUrl}/api/v1/projects/${projectRef}/envvars/${env}/${name}`;
+ };
+
+ /**
+ * Axios configuration with optional authorization.
+ */
+ const createAxiosConfig = (token?: string) => {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ }
+
+ return { headers };
+ };
+
+ /**
+ * Validate the response headers for JSON content type.
+ */
+ const expectJsonContentType = (response: AxiosResponse) => {
+ expect(response.headers['content-type']).toMatch(/application\/json/i);
+ };
+
+ beforeAll(() => {
+ apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
+ authToken = process.env.API_AUTH_TOKEN || '';
+ });
+
+ /**
+ * 1) Testing a successful update with valid data.
+ */
+ it('should update the environment variable successfully with valid data (200)', async () => {
+ const url = buildUrl(validProjectRef, validEnv, validName);
+ const requestData = {
+ value: 'UpdatedValue',
+ // Add any other required fields based on the actual schema, e.g. type/secret...
+ };
+
+ let response: AxiosResponse;
+ try {
+ response = await axios.put(url, requestData, createAxiosConfig(authToken));
+ expect(response.status).toBe(200);
+ expectJsonContentType(response);
+
+ // Example response schema check (Update according to actual SucceedResponse schema)
+ // For instance, if SucceedResponse has { success: boolean, message: string }
+ expect(response.data).toHaveProperty('success');
+ expect(response.data).toHaveProperty('message');
+ expect(response.data.success).toBe(true);
+ } catch (err: unknown) {
+ if (err instanceof AxiosError && err.response) {
+ // If this call unexpectedly fails, report details.
+ console.error('Unexpected error response:', err.response.data);
+ }
+ throw err;
+ }
+ });
+
+ /**
+ * 2) Testing invalid request body -> expect 400 or 422.
+ */
+ it('should return 400 or 422 for invalid request body', async () => {
+ const url = buildUrl(validProjectRef, validEnv, validName);
+
+ // Missing required fields or invalid data type.
+ const invalidRequestData = {
+ // e.g., missing "value" or using an invalid data type
+ value: 1234, // Suppose "value" must be a string based on the schema.
+ };
+
+ try {
+ await axios.put(url, invalidRequestData, createAxiosConfig(authToken));
+ // If the request does not fail, then we did not get the expected error.
+ throw new Error('Expected 400 or 422 error, but request succeeded.');
+ } catch (err: unknown) {
+ if (err instanceof AxiosError && err.response) {
+ expect([400, 422]).toContain(err.response.status);
+ expectJsonContentType(err.response);
+ // Example schema check for an error response
+ // Could be { error: string, details?: any } depending on actual schema
+ expect(err.response.data).toHaveProperty('error');
+ } else {
+ throw err;
+ }
+ }
+ });
+
+ /**
+ * 3) Testing missing or invalid auth token -> expect 401 or 403.
+ */
+ it('should return 401 or 403 if the auth token is missing or invalid', async () => {
+ const url = buildUrl(validProjectRef, validEnv, validName);
+
+ const requestData = {
+ value: 'AnyValue',
+ };
+
+ try {
+ await axios.put(url, requestData, createAxiosConfig('invalid-token'));
+ throw new Error('Expected 401 or 403 error, but request succeeded.');
+ } catch (err: unknown) {
+ if (err instanceof AxiosError && err.response) {
+ expect([401, 403]).toContain(err.response.status);
+ expectJsonContentType(err.response);
+ expect(err.response.data).toHaveProperty('error');
+ } else {
+ throw err;
+ }
+ }
+ });
+
+ /**
+ * 4) Testing resource not found -> expect 404.
+ */
+ it('should return 404 if the specified project/env/name does not exist', async () => {
+ const invalidProjectRef = 'doesNotExist';
+ const invalidEnv = 'notARealEnv';
+ const invalidName = 'UNKNOWN_VAR';
+
+ const url = buildUrl(invalidProjectRef, invalidEnv, invalidName);
+ const requestData = { value: 'AnyValue' };
+
+ try {
+ await axios.put(url, requestData, createAxiosConfig(authToken));
+ throw new Error('Expected 404 error, but request succeeded.');
+ } catch (err: unknown) {
+ if (err instanceof AxiosError && err.response) {
+ expect(err.response.status).toBe(404);
+ expectJsonContentType(err.response);
+ expect(err.response.data).toHaveProperty('error');
+ } else {
+ throw err;
+ }
+ }
+ });
+
+ /**
+ * 5) Testing large input data (edge case & limit testing).
+ * Provide an extremely long value.
+ */
+ it('should handle large payloads correctly', async () => {
+ const url = buildUrl(validProjectRef, validEnv, validName);
+
+ // 10,000 characters string as an example
+ const largeValue = 'X'.repeat(10000);
+ const requestData = {
+ value: largeValue,
+ };
+
+ try {
+ const response = await axios.put(url, requestData, createAxiosConfig(authToken));
+ // Depending on the API's limits, it may succeed or fail with 400.
+ // Adjust expectations as per your API specification.
+
+ // If we expect success:
+ expect(response.status).toBe(200);
+ expectJsonContentType(response);
+ expect(response.data).toHaveProperty('success');
+ expect(response.data.success).toBe(true);
+ } catch (err: unknown) {
+ if (err instanceof AxiosError && err.response) {
+ // If there's a size limit, we might get 400 or 413.
+ expect([400, 413]).toContain(err.response.status);
+ expectJsonContentType(err.response);
+ expect(err.response.data).toHaveProperty('error');
+ } else {
+ throw err;
+ }
+ }
+ });
+
+ /**
+ * 6) Testing behavior when no request body is provided.
+ */
+ it('should return 400 or 422 if no request body is provided', async () => {
+ const url = buildUrl(validProjectRef, validEnv, validName);
+
+ try {
+ await axios.put(url, {}, createAxiosConfig(authToken));
+ throw new Error('Expected 400 or 422 error, but request succeeded.');
+ } catch (err: unknown) {
+ if (err instanceof AxiosError && err.response) {
+ expect([400, 422]).toContain(err.response.status);
+ expectJsonContentType(err.response);
+ expect(err.response.data).toHaveProperty('error');
+ } else {
+ throw err;
+ }
+ }
+ });
+
+ /**
+ * 7) (Optional) Testing rate limiting (429) if applicable.
+ * This test is commented out by default because it might require multiple calls.
+ * Uncomment if the API has rate-limit enforcement in place.
+ */
+ // it('should return 429 if the request is rate-limited', async () => {
+ // const url = buildUrl(validProjectRef, validEnv, validName);
+ // const requestData = { value: 'RateTest' };
+ //
+ // for (let i = 0; i < 1000; i++) {
+ // try {
+ // await axios.put(url, requestData, createAxiosConfig(authToken));
+ // } catch (err: unknown) {
+ // if (err instanceof AxiosError && err.response) {
+ // if (err.response.status === 429) {
+ // expect(err.response.data).toHaveProperty('error');
+ // return;
+ // }
+ // }
+ // }
+ // }
+ // throw new Error('Expected 429 error, but request did not reach rate limit.');
+ // });
+
+ /**
+ * 8) (Optional) Testing server errors (5xx). This is hard to force from the client side.
+ * You could mock or intercept axios to simulate a 500 response.
+ */
+ // it('should handle 500 server error gracefully', async () => {
+ // // This test is typically done by mocking the API or error.
+ // });
+});
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.ts b/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.ts
new file mode 100644
index 0000000000..6d882c9d3e
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-runs-{runId}-metadata.ts
@@ -0,0 +1,175 @@
+import axios, { AxiosInstance, AxiosResponse } from 'axios';
+
+describe('PUT /api/v1/runs/:runId/metadata', () => {
+ let client: AxiosInstance;
+ // Adjust these IDs based on your actual data/environment
+ const validRunId = '12345'; // Replace with a known valid run ID if available
+ const invalidRunId = 'abc'; // Example of an invalid run ID
+ const nonExistentRunId = '99999'; // Example of a run ID that doesn't exist
+
+ beforeAll(() => {
+ const baseURL = process.env.API_BASE_URL || '';
+ const token = process.env.API_AUTH_TOKEN || '';
+
+ // Create an Axios instance with baseURL and authorization header
+ client = axios.create({
+ baseURL,
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ // validateStatus allows us to receive non-2xx responses without throwing
+ validateStatus: () => true,
+ });
+ });
+
+ it('should update run metadata with a valid payload (200)', async () => {
+ const payload = {
+ metadata: { exampleKey: 'exampleValue' },
+ };
+
+ // Make the PUT request with a valid runId and valid payload
+ const response: AxiosResponse = await client.put(`/api/v1/runs/${validRunId}/metadata`, payload);
+
+ // Check for expected 200 response
+ expect(response.status).toBe(200);
+ // Validate response headers
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ // Validate response body schema
+ expect(response.data).toHaveProperty('metadata');
+ expect(typeof response.data.metadata).toBe('object');
+ });
+
+ it('should return 401 or 403 for unauthorized or forbidden request', async () => {
+ // Create a client with no/invalid token
+ const unauthorizedClient = axios.create({
+ baseURL: process.env.API_BASE_URL || '',
+ validateStatus: () => true,
+ });
+
+ const payload = {
+ metadata: { exampleKey: 'exampleValue' },
+ };
+
+ const response: AxiosResponse = await unauthorizedClient.put(`/api/v1/runs/${validRunId}/metadata`, payload);
+
+ // The API might return 401 or 403
+ expect([401, 403]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ });
+
+ it('should return 400 or 422 for invalid payload (e.g., metadata is not an object)', async () => {
+ const payload = {
+ metadata: 'this_should_be_an_object', // Invalid type
+ };
+
+ const response: AxiosResponse = await client.put(`/api/v1/runs/${validRunId}/metadata`, payload);
+
+ // The API may return 400 or 422
+ expect([400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ });
+
+ it('should return 400 or 422 when the request body is empty', async () => {
+ const response: AxiosResponse = await client.put(`/api/v1/runs/${validRunId}/metadata`, {});
+
+ // The API may return 400 or 422
+ expect([400, 422]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ });
+
+ it('should return 400 if runId is invalid format', async () => {
+ const payload = {
+ metadata: { exampleKey: 'exampleValue' },
+ };
+
+ const response: AxiosResponse = await client.put(`/api/v1/runs/${invalidRunId}/metadata`, payload);
+
+ // Depending on the API, it might return 400 or 404
+ expect([400, 404]).toContain(response.status);
+ if (response.status === 400) {
+ // Validate error response
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ // Check one of the allowed error messages
+ expect(response.data.error).toMatch(/Invalid or missing run ID|Invalid metadata/);
+ }
+ if (response.status === 404) {
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ expect(response.data.error).toBe('Task Run not found');
+ }
+ });
+
+ it('should return 404 if run does not exist', async () => {
+ const payload = {
+ metadata: { exampleKey: 'exampleValue' },
+ };
+
+ const response: AxiosResponse = await client.put(`/api/v1/runs/${nonExistentRunId}/metadata`, payload);
+
+ // Some APIs may return 400 if runId is invalid, or 404 if not found
+ expect([400, 404]).toContain(response.status);
+ if (response.status === 404) {
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ expect(response.data.error).toBe('Task Run not found');
+ }
+ });
+
+ it('should handle large payload gracefully', async () => {
+ // Create a large metadata object to test boundary/limit scenarios
+ const largeMetadata: Record = {};
+ for (let i = 0; i < 10000; i++) {
+ largeMetadata[`key${i}`] = `value${i}`;
+ }
+
+ const payload = { metadata: largeMetadata };
+
+ const response: AxiosResponse = await client.put(`/api/v1/runs/${validRunId}/metadata`, payload);
+
+ // Could be 200 if successful, 400/413 if too large, etc.
+ expect([200, 400, 413]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ });
+
+ it('should return 401 if missing authorization header', async () => {
+ // Create a client with no auth header
+ const noAuthClient = axios.create({
+ baseURL: process.env.API_BASE_URL || '',
+ validateStatus: () => true,
+ });
+
+ const payload = {
+ metadata: { exampleKey: 'exampleValue' },
+ };
+
+ const response: AxiosResponse = await noAuthClient.put(`/api/v1/runs/${validRunId}/metadata`, payload);
+
+ // The API might return 401 or 403
+ expect([401, 403]).toContain(response.status);
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ expect(response.data.error).toMatch(/Invalid or Missing API key/);
+ });
+
+ it('should handle unexpected server errors (500) gracefully', async () => {
+ // There's no guaranteed way to trigger a 500, but we can try unusual payloads
+ const payload = {
+ metadata: { triggerServerError: true },
+ };
+
+ const response: AxiosResponse = await client.put(`/api/v1/runs/${validRunId}/metadata`, payload);
+
+ // If 500 occurs, validate the error body
+ if (response.status === 500) {
+ expect(response.headers['content-type']).toMatch(/application\\/json/);
+ expect(response.data).toHaveProperty('error');
+ } else {
+ // Otherwise we expect some valid status like 200, 400, or 422
+ expect([200, 400, 422].includes(response.status)).toBe(true);
+ }
+ });
+});
\ No newline at end of file
diff --git a/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.ts b/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.ts
new file mode 100644
index 0000000000..dae40ec276
--- /dev/null
+++ b/chapter_api_tests/2024-04/validation/test_put_api-v1-schedules-{schedule_id}.ts
@@ -0,0 +1,171 @@
+import axios from 'axios';
+import { AxiosResponse } from 'axios';
+
+describe('PUT /api/v1/schedules/{schedule_id}', () => {
+ let baseURL: string;
+ let authToken: string;
+ let validScheduleId: string;
+
+ beforeAll(() => {
+ // Load base URL and auth token from environment variables
+ baseURL = process.env.API_BASE_URL || '';
+ authToken = process.env.API_AUTH_TOKEN || '';
+
+ // For demonstration purposes, use a hardcoded or pre-created schedule ID.
+ // In a real test, you might retrieve this via a setup step.
+ validScheduleId = 'some-valid-schedule-id';
+ });
+
+ it('should update a schedule successfully with valid payload', async () => {
+ const url = `${baseURL}/api/v1/schedules/${validScheduleId}`;
+ const requestData = {
+ name: 'Updated Schedule',
+ type: 'IMPERATIVE',
+ // Include other valid fields as necessary
+ };
+
+ const response: AxiosResponse = await axios.put(url, requestData, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // Response status check
+ expect(response.status).toBe(200);
+ // Header validation
+ expect(response.headers['content-type']).toContain('application/json');
+ // Basic body validation (adjust to actual schema)
+ expect(response.data).toHaveProperty('id');
+ expect(response.data).toHaveProperty('name');
+ expect(response.data.name).toBe('Updated Schedule');
+ });
+
+ it('should return 400 or 422 for invalid request payload', async () => {
+ const url = `${baseURL}/api/v1/schedules/${validScheduleId}`;
+ // Example of invalid payload: wrong data type for 'name'
+ const invalidRequestData = {
+ name: 12345,
+ };
+
+ try {
+ await axios.put(url, invalidRequestData, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ fail('Request should have failed with status 400 or 422');
+ } catch (error: any) {
+ // Validate that we see one of the expected errors
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+
+ it('should return 401 or 403 for unauthorized or forbidden request', async () => {
+ const url = `${baseURL}/api/v1/schedules/${validScheduleId}`;
+ const validData = {
+ name: 'Attempted Update without Auth',
+ type: 'IMPERATIVE',
+ };
+
+ try {
+ // Missing Authorization header
+ await axios.put(url, validData, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ fail('Request should have failed with status 401 or 403');
+ } catch (error: any) {
+ expect([401, 403]).toContain(error.response.status);
+ }
+ });
+
+ it('should return 404 if the schedule does not exist', async () => {
+ const url = `${baseURL}/api/v1/schedules/non-existing-schedule-id`;
+ const requestData = {
+ name: 'Does Not Exist',
+ type: 'IMPERATIVE',
+ };
+
+ try {
+ await axios.put(url, requestData, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ fail('Request should have failed with status 404');
+ } catch (error: any) {
+ expect(error.response.status).toBe(404);
+ }
+ });
+
+ it('should return 400, 404, or 422 if path parameter is empty or invalid', async () => {
+ // Intentionally omitting the schedule_id
+ const url = `${baseURL}/api/v1/schedules/`;
+ const requestData = {
+ name: 'Should Fail',
+ type: 'IMPERATIVE',
+ };
+
+ try {
+ await axios.put(url, requestData, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ fail('Request should have failed due to invalid path');
+ } catch (error: any) {
+ expect([400, 404, 422]).toContain(error.response.status);
+ }
+ });
+
+ it('should return 400 or 422 for empty request body', async () => {
+ const url = `${baseURL}/api/v1/schedules/${validScheduleId}`;
+
+ try {
+ // Send an empty object
+ await axios.put(url, {}, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ fail('Request should have failed with status 400 or 422');
+ } catch (error: any) {
+ expect([400, 422]).toContain(error.response.status);
+ }
+ });
+
+ it('should handle large payload (if supported)', async () => {
+ const url = `${baseURL}/api/v1/schedules/${validScheduleId}`;
+ const largeName = 'A'.repeat(10000); // Extra-large string
+ const requestData = {
+ name: largeName,
+ type: 'IMPERATIVE',
+ };
+
+ try {
+ const response: AxiosResponse = await axios.put(url, requestData, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ // Some endpoints might accept large payloads; others might reject them.
+ // Check plausible success or error codes.
+ expect([200, 400, 413]).toContain(response.status);
+ } catch (error: any) {
+ if (error.response) {
+ // Validate typical error codes: 400, 413, or 422
+ expect([400, 413, 422]).toContain(error.response.status);
+ } else {
+ // Re-throw if there's a network or unexpected error
+ throw error;
+ }
+ }
+ });
+});
\ No newline at end of file