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