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