From 71516a795368089a5e26983874c9f06645ff86ba Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 17:09:35 +0100 Subject: [PATCH 01/20] 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 543383a28344301b8903d62de92d2575fa7f6b82 Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 17:37:21 +0100 Subject: [PATCH 02/20] Demo commit 2 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4a5d4773bc..a5de2782d9 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,4 @@ To setup and develop locally or contribute to the open source project, follow ou ## Test +- 1 From 9350aa8a58991b3005ab2552d1fd22af03918685 Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 19:30:56 +0100 Subject: [PATCH 03/20] Demo commit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a5de2782d9..70820a79b6 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,4 @@ To setup and develop locally or contribute to the open source project, follow ou ## Test - 1 +- 2 From 3be3e04947c44cf103b1fa443e66f958c96fae26 Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 20:00:04 +0100 Subject: [PATCH 04/20] Demo commit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 70820a79b6..91881605bf 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,4 @@ To setup and develop locally or contribute to the open source project, follow ou ## Test - 1 - 2 +- 3 From ab4b7258193c98a7ab10fb4279000437bff325ef Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:06:10 +0000 Subject: [PATCH 05/20] =?UTF-8?q?test(api):=20add=20api=20tests=20--=20Cha?= =?UTF-8?q?pter=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/test_post_api-v1-schedules.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts 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..d50680d466 --- /dev/null +++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts @@ -0,0 +1,99 @@ +import axios, { AxiosInstance } from 'axios'; +describe('POST /api/v1/schedules', () => { + let axiosInstance: AxiosInstance; + + beforeAll(() => { + axiosInstance = axios.create({ + baseURL: process.env.API_BASE_URL, + headers: { + Authorization: 'Bearer ' + (process.env.API_AUTH_TOKEN || ''), + 'Content-Type': 'application/json', + }, + }); + }); + + // 1. Valid payload -> Expect 200 + it('should create a schedule with valid payload and respond with 200', async () => { + const validSchedule = { + name: 'Test Schedule', + type: 'IMPERATIVE', + description: 'This is a test schedule', + startDate: '2023-10-10T00:00:00Z', + endDate: '2023-10-11T00:00:00Z', + }; + + const response = await axiosInstance.post('/api/v1/schedules', validSchedule); + expect(response.status).toBe(200); + // 2. Response Validation + expect(response.data).toHaveProperty('id'); + expect(response.data.name).toBe(validSchedule.name); + // 3. Response Headers Validation + expect(response.headers['content-type']).toMatch(/application\/json/); + }); + + // 1. Missing required fields -> Expect 400 or 422 + it('should return 400 or 422 when required fields are missing', async () => { + const invalidSchedule = { + // 'name' is required but missing + type: 'IMPERATIVE', + }; + + try { + await axiosInstance.post('/api/v1/schedules', invalidSchedule); + fail('Request should have thrown an error'); + } catch (error: any) { + expect([400, 422]).toContain(error.response.status); + } + }); + + // 1. Invalid data types -> Expect 400 or 422 + it('should return 400 or 422 for invalid data types', async () => { + const invalidSchedule = { + name: 12345, // should be string + type: 'IMPERATIVE', + }; + + try { + await axiosInstance.post('/api/v1/schedules', invalidSchedule); + fail('Request should have thrown an error'); + } catch (error: any) { + expect([400, 422]).toContain(error.response.status); + } + }); + + // 5. Authorization & Authentication -> Expect 401 or 403 + it('should return 401 or 403 for unauthorized requests', async () => { + const validSchedule = { + name: 'Unauthorized Schedule', + type: 'IMPERATIVE', + }; + + try { + await axios.post((process.env.API_BASE_URL || '') + '/api/v1/schedules', validSchedule, { + headers: { + Authorization: 'Bearer invalid_token', + 'Content-Type': 'application/json', + }, + }); + fail('Request should have thrown an error'); + } catch (error: any) { + expect([401, 403]).toContain(error.response.status); + } + }); + + // 4. Edge Case: Large payload -> Expect possible 400 or 422 + it('should handle large payload gracefully', async () => { + const longString = 'x'.repeat(10000); + const invalidSchedule = { + name: longString, + type: 'IMPERATIVE', + }; + + try { + await axiosInstance.post('/api/v1/schedules', invalidSchedule); + // Depending on service limits, might succeed or return an error. + } catch (error: any) { + expect([400, 422]).toContain(error.response.status); + } + }); +}); \ No newline at end of file From 84ff0daacf7b6001b52fff75635232b17c05e793 Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:06:11 +0000 Subject: [PATCH 06/20] =?UTF-8?q?ci(apitest):=20add=20Github=20Actions=20w?= =?UTF-8?q?orkflow=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..947d588327 --- /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" From 74e5c2d6ef67c238ff477789041bdf97078a2de8 Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 20:16:09 +0100 Subject: [PATCH 07/20] Demo commit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 91881605bf..e0fc0d794f 100644 --- a/README.md +++ b/README.md @@ -94,3 +94,4 @@ To setup and develop locally or contribute to the open source project, follow ou - 1 - 2 - 3 +- 4 From f02c45abd60ef83eb4114eb862aaad343ece8833 Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:18:51 +0000 Subject: [PATCH 08/20] =?UTF-8?q?test(api):=20add=20api=20tests=20--=20Cha?= =?UTF-8?q?pter=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/test_post_api-v1-schedules.ts | 228 +++++++++++++----- 1 file changed, 166 insertions(+), 62 deletions(-) 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 index d50680d466..81f99739f4 100644 --- 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 @@ -1,99 +1,203 @@ -import axios, { AxiosInstance } from 'axios'; +import axios, { AxiosInstance, AxiosResponse } from 'axios'; + +// Utility function to create an Axios instance with base URL and auth token +function createAxiosInstance(authToken?: string): AxiosInstance { + return axios.create({ + baseURL: process.env.API_BASE_URL, + headers: { + ...(authToken && { Authorization: `Bearer ${authToken}` }), + 'Content-Type': 'application/json', + }, + }); +} + +/** + * Example Schedule creation payload. + * Adjust fields according to the actual OpenAPI schema if needed. + */ +const validSchedulePayload = { + name: 'Test Schedule', + type: 'IMPERATIVE', + // Add any other required fields here + // e.g., startTime, endTime, etc. + // startTime: '2023-01-01T09:00:00Z', + // endTime: '2023-01-01T17:00:00Z', +}; + describe('POST /api/v1/schedules', () => { let axiosInstance: AxiosInstance; beforeAll(() => { - axiosInstance = axios.create({ - baseURL: process.env.API_BASE_URL, - headers: { - Authorization: 'Bearer ' + (process.env.API_AUTH_TOKEN || ''), - 'Content-Type': 'application/json', - }, - }); + // Create an axios instance with valid auth token from environment + axiosInstance = createAxiosInstance(process.env.API_AUTH_TOKEN); }); - // 1. Valid payload -> Expect 200 - it('should create a schedule with valid payload and respond with 200', async () => { - const validSchedule = { - name: 'Test Schedule', - type: 'IMPERATIVE', - description: 'This is a test schedule', - startDate: '2023-10-10T00:00:00Z', - endDate: '2023-10-11T00:00:00Z', - }; + /** + * 1. Valid request + * - Should create schedule with valid data. + * - Expect 200, check response body structure, headers, etc. + */ + it('should create a schedule with valid data (expect 200)', async () => { + const response: AxiosResponse = await axiosInstance.post('/api/v1/schedules', validSchedulePayload); - const response = await axiosInstance.post('/api/v1/schedules', validSchedule); + // Response Validation expect(response.status).toBe(200); - // 2. Response Validation - expect(response.data).toHaveProperty('id'); - expect(response.data.name).toBe(validSchedule.name); - // 3. Response Headers Validation expect(response.headers['content-type']).toMatch(/application\/json/); + + // Basic body/schema checks (add more detailed checks according to the actual schema) + expect(response.data).toBeDefined(); + // For example, if the returned schedule has an id, type, name, etc. + // expect(response.data).toHaveProperty('id'); + // expect(response.data.type).toBe('IMPERATIVE'); }); - // 1. Missing required fields -> Expect 400 or 422 - it('should return 400 or 422 when required fields are missing', async () => { - const invalidSchedule = { - // 'name' is required but missing - type: 'IMPERATIVE', + /** + * 2. Missing or invalid fields (body validation) + * - Should return 400 or 422 for invalid data. + */ + it('should reject invalid payload (expect 400 or 422)', async () => { + const invalidPayload = { + // Missing required fields or incorrect types + // e.g., type is an invalid string + type: 'UNKNOWN_TYPE', }; try { - await axiosInstance.post('/api/v1/schedules', invalidSchedule); - fail('Request should have thrown an error'); + await axiosInstance.post('/api/v1/schedules', invalidPayload); + // If the request unexpectedly succeeds, fail the test + fail('Request should have failed with a 400 or 422 status.'); } catch (error: any) { - expect([400, 422]).toContain(error.response.status); + // Axios attaches status code to error.response.status + expect([400, 422]).toContain(error?.response?.status); } }); - // 1. Invalid data types -> Expect 400 or 422 - it('should return 400 or 422 for invalid data types', async () => { - const invalidSchedule = { - name: 12345, // should be string - type: 'IMPERATIVE', - }; + /** + * 3. Authorization & Authentication Tests + * - If token is missing or invalid, should return 401 or 403. + */ + it('should return 401 or 403 if auth token is missing or invalid', async () => { + // Create an axios instance without a valid token + const unauthenticatedAxios = createAxiosInstance('invalid-token'); try { - await axiosInstance.post('/api/v1/schedules', invalidSchedule); - fail('Request should have thrown an error'); + await unauthenticatedAxios.post('/api/v1/schedules', validSchedulePayload); + fail('Request should have failed due to authentication issues.'); } catch (error: any) { - expect([400, 422]).toContain(error.response.status); + expect([401, 403]).toContain(error?.response?.status); } }); - // 5. Authorization & Authentication -> Expect 401 or 403 - it('should return 401 or 403 for unauthorized requests', async () => { - const validSchedule = { - name: 'Unauthorized Schedule', - type: 'IMPERATIVE', + /** + * 4. Edge Case & Limit Testing + * - Large payloads, boundary values, empty strings. + * - Ensure we get a valid error (400 or 422) or success if the API allows it. + */ + it('should handle large payloads (boundary testing)', async () => { + const largeName = 'A'.repeat(10000); // Very long name + const largePayload = { + ...validSchedulePayload, + name: largeName, }; + // Some APIs may accept large strings, some may fail + // We expect either success (200) or a client error (400/422) if the payload is too large. try { - await axios.post((process.env.API_BASE_URL || '') + '/api/v1/schedules', validSchedule, { - headers: { - Authorization: 'Bearer invalid_token', - 'Content-Type': 'application/json', - }, - }); - fail('Request should have thrown an error'); + const response = await axiosInstance.post('/api/v1/schedules', largePayload); + // If successful, check response status and body + expect(response.status).toBe(200); + expect(response.data).toBeDefined(); + expect(response.headers['content-type']).toMatch(/application\/json/); } catch (error: any) { - expect([401, 403]).toContain(error.response.status); + if (error?.response?.status) { + // Acceptable error codes for large payload issues + expect([400, 422]).toContain(error.response.status); + } else { + throw error; // If it's another error, re-throw + } } }); - // 4. Edge Case: Large payload -> Expect possible 400 or 422 - it('should handle large payload gracefully', async () => { - const longString = 'x'.repeat(10000); - const invalidSchedule = { - name: longString, - type: 'IMPERATIVE', + it('should handle empty string or null fields (boundary testing)', async () => { + const emptyFieldPayload = { + ...validSchedulePayload, + name: '', // Empty where name is potentially required }; try { - await axiosInstance.post('/api/v1/schedules', invalidSchedule); - // Depending on service limits, might succeed or return an error. + await axiosInstance.post('/api/v1/schedules', emptyFieldPayload); + fail('Request with empty required field should fail.'); + } catch (error: any) { + expect([400, 422]).toContain(error?.response?.status); + } + }); + + /** + * 5. Malformed requests, server errors. + * - 500 errors are typically internal server errors, but we can test a malformed request scenario. + * - This is an example; forcing a 500 depends on server logic. + */ + it('should gracefully handle invalid JSON (malformed request)', async () => { + // Send a malformed JSON string. We simulate by using a custom request. + try { + const response = await axiosInstance.post( + '/api/v1/schedules', + '"malformed JSON" : test', // This is intentionally malformed + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + // If the server tries to parse and fails, it may throw 400 or 422 + expect([400, 422]).toContain(response.status); + } catch (error: any) { + if (error?.response?.status) { + expect([400, 422]).toContain(error.response.status); + } else { + // If a 500 or other code occurs, it's also possible + expect([400, 422, 500]).toContain(error.response.status); + } + } + }); + + /** + * 6. Response Headers Validation + * - Confirm Content-Type = application/json, etc. + * - For either success or error, the Content-Type may differ, but we typically expect JSON. + */ + it('should return JSON content-type header on valid request', async () => { + const response: AxiosResponse = await axiosInstance.post('/api/v1/schedules', validSchedulePayload); + + expect(response.headers['content-type']).toMatch(/application\/json/); + }); + + /** + * Additional tests like no results found (not directly applicable to POST create), + * but included if the server logic might return an empty list or object. + * Typically, 200 with an empty object if, for instance, no schedule data is created. + */ + it('should handle scenario if the server returns an empty object', async () => { + // This test is speculative. If the API can respond with an empty object, handle the check. + // Force a condition that might return empty data if possible. + + try { + const response: AxiosResponse = await axiosInstance.post( + '/api/v1/schedules', + { + // Possibly some condition that leads to an empty response. + // Implementation depends on actual server logic. + name: 'Name that causes empty response', + type: 'IMPERATIVE', + } + ); + // Validate that if we get a 200, the body might be empty. + expect(response.status).toBe(200); + // If the server returns an empty object + expect(response.data).toBeDefined(); } catch (error: any) { - expect([400, 422]).toContain(error.response.status); + // Not expected but handle possible 400,422 etc. + expect([400, 422]).toContain(error?.response?.status); } }); -}); \ No newline at end of file +}); From d0499aefcb6e7773b1ba3a0900a96b65b03c254b Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:18:52 +0000 Subject: [PATCH 09/20] =?UTF-8?q?ci(apitest):=20add=20Github=20Actions=20w?= =?UTF-8?q?orkflow=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 From d5d5e0383b5a03332e815f8b441daa4b760fd3c2 Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 20:32:49 +0100 Subject: [PATCH 10/20] Demo commit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e0fc0d794f..86245ec723 100644 --- a/README.md +++ b/README.md @@ -95,3 +95,4 @@ To setup and develop locally or contribute to the open source project, follow ou - 2 - 3 - 4 +- 5 From ec9fe81e0680761dcc06094b3ec5c193be520dbc Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:38:09 +0000 Subject: [PATCH 11/20] =?UTF-8?q?test(api):=20add=20api=20tests=20--=20Cha?= =?UTF-8?q?pter=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/test_post_api-v1-schedules.ts | 204 +----------------- 1 file changed, 1 insertion(+), 203 deletions(-) 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 index 81f99739f4..cd881af78a 100644 --- 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 @@ -1,203 +1 @@ -import axios, { AxiosInstance, AxiosResponse } from 'axios'; - -// Utility function to create an Axios instance with base URL and auth token -function createAxiosInstance(authToken?: string): AxiosInstance { - return axios.create({ - baseURL: process.env.API_BASE_URL, - headers: { - ...(authToken && { Authorization: `Bearer ${authToken}` }), - 'Content-Type': 'application/json', - }, - }); -} - -/** - * Example Schedule creation payload. - * Adjust fields according to the actual OpenAPI schema if needed. - */ -const validSchedulePayload = { - name: 'Test Schedule', - type: 'IMPERATIVE', - // Add any other required fields here - // e.g., startTime, endTime, etc. - // startTime: '2023-01-01T09:00:00Z', - // endTime: '2023-01-01T17:00:00Z', -}; - -describe('POST /api/v1/schedules', () => { - let axiosInstance: AxiosInstance; - - beforeAll(() => { - // Create an axios instance with valid auth token from environment - axiosInstance = createAxiosInstance(process.env.API_AUTH_TOKEN); - }); - - /** - * 1. Valid request - * - Should create schedule with valid data. - * - Expect 200, check response body structure, headers, etc. - */ - it('should create a schedule with valid data (expect 200)', async () => { - const response: AxiosResponse = await axiosInstance.post('/api/v1/schedules', validSchedulePayload); - - // Response Validation - expect(response.status).toBe(200); - expect(response.headers['content-type']).toMatch(/application\/json/); - - // Basic body/schema checks (add more detailed checks according to the actual schema) - expect(response.data).toBeDefined(); - // For example, if the returned schedule has an id, type, name, etc. - // expect(response.data).toHaveProperty('id'); - // expect(response.data.type).toBe('IMPERATIVE'); - }); - - /** - * 2. Missing or invalid fields (body validation) - * - Should return 400 or 422 for invalid data. - */ - it('should reject invalid payload (expect 400 or 422)', async () => { - const invalidPayload = { - // Missing required fields or incorrect types - // e.g., type is an invalid string - type: 'UNKNOWN_TYPE', - }; - - try { - await axiosInstance.post('/api/v1/schedules', invalidPayload); - // If the request unexpectedly succeeds, fail the test - fail('Request should have failed with a 400 or 422 status.'); - } catch (error: any) { - // Axios attaches status code to error.response.status - expect([400, 422]).toContain(error?.response?.status); - } - }); - - /** - * 3. Authorization & Authentication Tests - * - If token is missing or invalid, should return 401 or 403. - */ - it('should return 401 or 403 if auth token is missing or invalid', async () => { - // Create an axios instance without a valid token - const unauthenticatedAxios = createAxiosInstance('invalid-token'); - - try { - await unauthenticatedAxios.post('/api/v1/schedules', validSchedulePayload); - fail('Request should have failed due to authentication issues.'); - } catch (error: any) { - expect([401, 403]).toContain(error?.response?.status); - } - }); - - /** - * 4. Edge Case & Limit Testing - * - Large payloads, boundary values, empty strings. - * - Ensure we get a valid error (400 or 422) or success if the API allows it. - */ - it('should handle large payloads (boundary testing)', async () => { - const largeName = 'A'.repeat(10000); // Very long name - const largePayload = { - ...validSchedulePayload, - name: largeName, - }; - - // Some APIs may accept large strings, some may fail - // We expect either success (200) or a client error (400/422) if the payload is too large. - try { - const response = await axiosInstance.post('/api/v1/schedules', largePayload); - // If successful, check response status and body - expect(response.status).toBe(200); - expect(response.data).toBeDefined(); - expect(response.headers['content-type']).toMatch(/application\/json/); - } catch (error: any) { - if (error?.response?.status) { - // Acceptable error codes for large payload issues - expect([400, 422]).toContain(error.response.status); - } else { - throw error; // If it's another error, re-throw - } - } - }); - - it('should handle empty string or null fields (boundary testing)', async () => { - const emptyFieldPayload = { - ...validSchedulePayload, - name: '', // Empty where name is potentially required - }; - - try { - await axiosInstance.post('/api/v1/schedules', emptyFieldPayload); - fail('Request with empty required field should fail.'); - } catch (error: any) { - expect([400, 422]).toContain(error?.response?.status); - } - }); - - /** - * 5. Malformed requests, server errors. - * - 500 errors are typically internal server errors, but we can test a malformed request scenario. - * - This is an example; forcing a 500 depends on server logic. - */ - it('should gracefully handle invalid JSON (malformed request)', async () => { - // Send a malformed JSON string. We simulate by using a custom request. - try { - const response = await axiosInstance.post( - '/api/v1/schedules', - '"malformed JSON" : test', // This is intentionally malformed - { - headers: { - 'Content-Type': 'application/json', - }, - } - ); - // If the server tries to parse and fails, it may throw 400 or 422 - expect([400, 422]).toContain(response.status); - } catch (error: any) { - if (error?.response?.status) { - expect([400, 422]).toContain(error.response.status); - } else { - // If a 500 or other code occurs, it's also possible - expect([400, 422, 500]).toContain(error.response.status); - } - } - }); - - /** - * 6. Response Headers Validation - * - Confirm Content-Type = application/json, etc. - * - For either success or error, the Content-Type may differ, but we typically expect JSON. - */ - it('should return JSON content-type header on valid request', async () => { - const response: AxiosResponse = await axiosInstance.post('/api/v1/schedules', validSchedulePayload); - - expect(response.headers['content-type']).toMatch(/application\/json/); - }); - - /** - * Additional tests like no results found (not directly applicable to POST create), - * but included if the server logic might return an empty list or object. - * Typically, 200 with an empty object if, for instance, no schedule data is created. - */ - it('should handle scenario if the server returns an empty object', async () => { - // This test is speculative. If the API can respond with an empty object, handle the check. - // Force a condition that might return empty data if possible. - - try { - const response: AxiosResponse = await axiosInstance.post( - '/api/v1/schedules', - { - // Possibly some condition that leads to an empty response. - // Implementation depends on actual server logic. - name: 'Name that causes empty response', - type: 'IMPERATIVE', - } - ); - // Validate that if we get a 200, the body might be empty. - expect(response.status).toBe(200); - // If the server returns an empty object - expect(response.data).toBeDefined(); - } catch (error: any) { - // Not expected but handle possible 400,422 etc. - expect([400, 422]).toContain(error?.response?.status); - } - }); -}); +import axios, { AxiosInstance, AxiosError } from 'axios';\nimport { describe, it, expect, beforeAll } from '@jest/globals';\n\ndescribe('POST /api/v1/schedules', () => {\n let client: AxiosInstance;\n const baseURL = process.env.API_BASE_URL;\n const authToken = process.env.API_AUTH_TOKEN;\n\n beforeAll(() => {\n client = axios.create({\n baseURL,\n headers: { Authorization: `Bearer ${authToken}` },\n });\n });\n\n describe('Valid Request', () => {\n it('should create a schedule with a valid payload', async () => {\n const payload = {\n name: 'Test Schedule',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n const response = await client.post('/api/v1/schedules', payload);\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n\n // Basic response body validation (adjust based on actual schema)\n expect(response.data).toHaveProperty('id');\n expect(response.data).toHaveProperty('name', payload.name);\n expect(response.data).toHaveProperty('type', payload.type);\n expect(response.data).toHaveProperty('startTime', payload.startTime);\n });\n });\n\n describe('Input Validation', () => {\n it('should return 400 or 422 when required field \"type\" is missing', async () => {\n const invalidPayload = {\n // Missing 'type'\n name: 'Test Schedule',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await client.post('/api/v1/schedules', invalidPayload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should return 400 or 422 when \"type\" is not \"IMPERATIVE\"', async () => {\n const invalidPayload = {\n name: 'Wrong type schedule',\n type: 'INVALID_TYPE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await client.post('/api/v1/schedules', invalidPayload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should return 400 or 422 for an empty string field', async () => {\n const invalidPayload = {\n name: '',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await client.post('/api/v1/schedules', invalidPayload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n });\n\n describe('Response Headers Validation', () => {\n it('should return application/json content-type for a valid request', async () => {\n const payload = {\n name: 'Header Test',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n const response = await client.post('/api/v1/schedules', payload);\n expect(response.headers['content-type']).toContain('application/json');\n });\n });\n\n describe('Edge Case & Limit Testing', () => {\n it('should return 401 or 403 if no auth token is provided', async () => {\n const unauthorizedClient = axios.create({ baseURL });\n const payload = {\n name: 'Unauthorized Test',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await unauthorizedClient.post('/api/v1/schedules', payload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([401, 403]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should handle large payload gracefully', async () => {\n const largeName = 'A'.repeat(10000); // example of large input\n const payload = {\n name: largeName,\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n const response = await client.post('/api/v1/schedules', payload);\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n // Acceptable responses could be 400, 413, 422, etc. depending on server config\n expect([400, 413, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should handle malformed JSON request', async () => {\n // We'll simulate malformed JSON by sending a string instead of an object\n try {\n await client.post('/api/v1/schedules', '{ invalidJson: true');\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n });\n}); \ No newline at end of file From 3668028aff456d6b78a26be1daf97c3eecfe6131 Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:38:10 +0000 Subject: [PATCH 12/20] =?UTF-8?q?ci(apitest):=20add=20Github=20Actions=20w?= =?UTF-8?q?orkflow=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 From ef5ed74900008e0ee5e9a42b5660c69da7f13276 Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 20:45:12 +0100 Subject: [PATCH 13/20] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 86245ec723..25e88ffaaf 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,4 @@ To setup and develop locally or contribute to the open source project, follow ou - 3 - 4 - 5 +- 6 From aec1bd398d09e39d467b8289aef5d05ce7b97147 Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 20:49:14 +0100 Subject: [PATCH 14/20] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 25e88ffaaf..9a9c46d651 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,4 @@ To setup and develop locally or contribute to the open source project, follow ou - 4 - 5 - 6 +- 7 From 4bf2251621651b5127d632733aa0a59cdca7ddbd Mon Sep 17 00:00:00 2001 From: Shri Date: Wed, 19 Feb 2025 20:56:16 +0100 Subject: [PATCH 15/20] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9a9c46d651..cd74485725 100644 --- a/README.md +++ b/README.md @@ -98,3 +98,4 @@ To setup and develop locally or contribute to the open source project, follow ou - 5 - 6 - 7 +- 8 From 113ae85a47814bde6f4b1bc3500d15cf31d4af11 Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:58:28 +0000 Subject: [PATCH 16/20] =?UTF-8?q?test(api):=20add=20api=20tests=20--=20Cha?= =?UTF-8?q?pter=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/test_post_api-v1-schedules.ts | 247 +++++++++++++++++- 1 file changed, 246 insertions(+), 1 deletion(-) 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 index cd881af78a..2b79bcdec2 100644 --- 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 @@ -1 +1,246 @@ -import axios, { AxiosInstance, AxiosError } from 'axios';\nimport { describe, it, expect, beforeAll } from '@jest/globals';\n\ndescribe('POST /api/v1/schedules', () => {\n let client: AxiosInstance;\n const baseURL = process.env.API_BASE_URL;\n const authToken = process.env.API_AUTH_TOKEN;\n\n beforeAll(() => {\n client = axios.create({\n baseURL,\n headers: { Authorization: `Bearer ${authToken}` },\n });\n });\n\n describe('Valid Request', () => {\n it('should create a schedule with a valid payload', async () => {\n const payload = {\n name: 'Test Schedule',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n const response = await client.post('/api/v1/schedules', payload);\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n\n // Basic response body validation (adjust based on actual schema)\n expect(response.data).toHaveProperty('id');\n expect(response.data).toHaveProperty('name', payload.name);\n expect(response.data).toHaveProperty('type', payload.type);\n expect(response.data).toHaveProperty('startTime', payload.startTime);\n });\n });\n\n describe('Input Validation', () => {\n it('should return 400 or 422 when required field \"type\" is missing', async () => {\n const invalidPayload = {\n // Missing 'type'\n name: 'Test Schedule',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await client.post('/api/v1/schedules', invalidPayload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should return 400 or 422 when \"type\" is not \"IMPERATIVE\"', async () => {\n const invalidPayload = {\n name: 'Wrong type schedule',\n type: 'INVALID_TYPE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await client.post('/api/v1/schedules', invalidPayload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should return 400 or 422 for an empty string field', async () => {\n const invalidPayload = {\n name: '',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await client.post('/api/v1/schedules', invalidPayload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n });\n\n describe('Response Headers Validation', () => {\n it('should return application/json content-type for a valid request', async () => {\n const payload = {\n name: 'Header Test',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n const response = await client.post('/api/v1/schedules', payload);\n expect(response.headers['content-type']).toContain('application/json');\n });\n });\n\n describe('Edge Case & Limit Testing', () => {\n it('should return 401 or 403 if no auth token is provided', async () => {\n const unauthorizedClient = axios.create({ baseURL });\n const payload = {\n name: 'Unauthorized Test',\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n await unauthorizedClient.post('/api/v1/schedules', payload);\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([401, 403]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should handle large payload gracefully', async () => {\n const largeName = 'A'.repeat(10000); // example of large input\n const payload = {\n name: largeName,\n type: 'IMPERATIVE',\n startTime: '2023-10-01T09:00:00Z',\n };\n\n try {\n const response = await client.post('/api/v1/schedules', payload);\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/json');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n // Acceptable responses could be 400, 413, 422, etc. depending on server config\n expect([400, 413, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n\n it('should handle malformed JSON request', async () => {\n // We'll simulate malformed JSON by sending a string instead of an object\n try {\n await client.post('/api/v1/schedules', '{ invalidJson: true');\n fail('Request should have failed');\n } catch (error) {\n const axiosError = error as AxiosError;\n if (axiosError.response) {\n expect([400, 422]).toContain(axiosError.response.status);\n } else {\n throw error;\n }\n }\n });\n });\n}); \ No newline at end of file +import axios, { AxiosResponse } from 'axios'; +import { describe, it, expect, beforeAll } from '@jest/globals'; + +/** + * Jest test suite for POST /api/v1/schedules + * Requirements: + * 1. Input Validation + * 2. Response Validation + * 3. Response Headers Validation + * 4. Edge Case & Limit Testing + * 5. Authorization & Authentication + * + * - Loads API base URL from process.env.API_BASE_URL + * - Loads auth token from process.env.API_AUTH_TOKEN + * - Uses axios as HTTP client + * - Written in TypeScript (with Jest as test framework) + * - Ensures Prettier style formatting + */ + +describe('POST /api/v1/schedules', () => { + let baseURL: string; + let authToken: string; + + beforeAll(() => { + baseURL = process.env.API_BASE_URL || 'http://localhost:3000'; + authToken = process.env.API_AUTH_TOKEN || ''; + }); + + /** + * Helper function to create a schedule. + * Attaches Authorization header if provided. + */ + const createSchedule = async ( + payload: Record, + token?: string + ): Promise => { + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + return axios.post(`${baseURL}/api/v1/schedules`, payload, { headers }); + }; + + /** + * TEST 1: Valid Payload (Happy Path) + * + * - Expects 200 response code ("Schedule created successfully"). + * - Verifies JSON response matches expected schema. + * - Checks response headers. + */ + it('should create a schedule with valid payload', async () => { + const validPayload = { + // Example payload conforming to #/components/schemas/ScheduleObject + // Adjust fields according to your actual schema. + type: 'IMPERATIVE', + title: 'Automated Test Schedule', + startTime: '2023-01-01T00:00:00Z', + endTime: '2023-01-01T01:00:00Z', + // Add other required fields as needed + }; + + const response = await createSchedule(validPayload, authToken); + + // Response Validation + expect(response.status).toBe(200); + expect(response.headers['content-type']).toContain('application/json'); + + // Sample checks against the response body schema + // Adjust these checks depending on your actual ScheduleObject schema. + expect(response.data).toHaveProperty('id'); + expect(response.data).toHaveProperty('type', 'IMPERATIVE'); + expect(response.data).toHaveProperty('title', 'Automated Test Schedule'); + }); + + /** + * TEST 2: Missing Required Fields => 400 or 422 + * + * - Expects error code 400 or 422 when payload is invalid. + */ + it('should return 400 or 422 for invalid payload (missing fields)', async () => { + const invalidPayload = { + // Omit required fields to trigger validation error + type: 'IMPERATIVE', + // title missing, startTime missing, etc. + }; + + try { + await createSchedule(invalidPayload, authToken); + // If request succeeds unexpectedly + fail('Expected request to fail for invalid payload, but it succeeded.'); + } catch (error: any) { + const status = error.response?.status; + // The API might respond with 400 or 422 for invalid payload. + expect([400, 422]).toContain(status); + } + }); + + /** + * TEST 3: Wrong Data Types => 400 or 422 + * + * - Expects error code 400 or 422 when data types are incorrect. + */ + it('should return 400 or 422 for incorrect data types', async () => { + const invalidPayload = { + type: 'IMPERATIVE', + title: 1234, // title should be a string + startTime: true, // startTime should be a string/timestamp, not boolean + endTime: null, // etc. + }; + + try { + await createSchedule(invalidPayload, authToken); + fail('Expected request to fail for incorrect data types, but it succeeded.'); + } catch (error: any) { + const status = error.response?.status; + expect([400, 422]).toContain(status); + } + }); + + /** + * TEST 4: Unauthorized Access => 401 or 403 + * + * - No token or invalid token should yield 401 or 403. + */ + it('should return 401 or 403 for unauthorized (missing or invalid token)', async () => { + const validPayload = { + type: 'IMPERATIVE', + title: 'Unauthorized Test Schedule', + startTime: '2023-01-01T02:00:00Z', + endTime: '2023-01-01T03:00:00Z', + }; + + try { + // Call without token + await createSchedule(validPayload); + fail('Expected 401 or 403 for unauthorized access, but it succeeded.'); + } catch (error: any) { + const status = error.response?.status; + expect([401, 403]).toContain(status); + } + }); + + /** + * TEST 5: Empty Request Body => 400 or 422 + * + * - Expects the API to reject empty body. + */ + it('should return 400 or 422 when request body is empty', async () => { + try { + await createSchedule({}, authToken); + fail('Expected 400 or 422 for empty body, but request succeeded.'); + } catch (error: any) { + const status = error.response?.status; + expect([400, 422]).toContain(status); + } + }); + + /** + * TEST 6: Large Payload => check for 400, 413 (Payload Too Large), or success + * + * - Sends a large string to ensure the API handles or rejects appropriately. + */ + it('should handle large payload gracefully', async () => { + // Generate a large string. + const largeString = 'x'.repeat(10000); // 10k characters + + const largePayload = { + type: 'IMPERATIVE', + title: largeString, + startTime: '2023-01-02T00:00:00Z', + endTime: '2023-01-02T01:00:00Z', + }; + + let responseStatus: number = 0; + + try { + const response = await createSchedule(largePayload, authToken); + responseStatus = response.status; + } catch (error: any) { + responseStatus = error.response?.status; + } + + // The API might accept it (200) or reject it (400, 413, 422, etc.). + // Adjust as appropriate for your API's expected behavior. + expect([200, 400, 413, 422]).toContain(responseStatus); + }); + + /** + * TEST 7: Endpoint Not Found => 404 + * + * - Calls a non-existent path to ensure we get a 404. + */ + it('should return 404 for invalid endpoint', async () => { + try { + await axios.post(`${baseURL}/api/v1/schedulezzz`, {}, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); + fail('Expected 404 for invalid endpoint, but request succeeded.'); + } catch (error: any) { + expect(error.response?.status).toBe(404); + } + }); + + /** + * TEST 8: Server Error Handling => 500 + * + * - This test is illustrative if your API might throw a 500 for certain conditions. + * Adjust to match a scenario that triggers a 500 in your environment. + */ + it('should handle 500 Internal Server Error (if applicable)', async () => { + // This scenario is highly dependent on your API's implementation. + // Example approach: pass in data that forces a server error. + + const erroneousPayload = { + type: 'IMPERATIVE', + title: 'Trigger Server Error', + // Suppose passing a specific field or invalid data triggers a 500. + forceServerError: true, + }; + + let statusCode: number | undefined; + + try { + await createSchedule(erroneousPayload, authToken); + } catch (error: any) { + statusCode = error.response?.status; + } + + // If the API can return 500 under certain conditions + // We check if that scenario can occur. + // Remove or adjust this test if 500 isn't expected. + if (statusCode === 500) { + expect(statusCode).toBe(500); + } else { + // If your API won't return 500 for such scenarios, you may want to adjust expectations. + expect([400, 422, 500]).toContain(statusCode); + } + }); +}); From 13456945f077558203353e33a73c7b88b3722403 Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Wed, 19 Feb 2025 19:58:29 +0000 Subject: [PATCH 17/20] =?UTF-8?q?ci(apitest):=20add=20Github=20Actions=20w?= =?UTF-8?q?orkflow=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 From 990fd327f150a25086b146815e5e85c843a9297c Mon Sep 17 00:00:00 2001 From: Shri Date: Thu, 20 Feb 2025 08:54:50 +0100 Subject: [PATCH 18/20] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cd74485725..ceca49c0ba 100644 --- a/README.md +++ b/README.md @@ -99,3 +99,4 @@ To setup and develop locally or contribute to the open source project, follow ou - 6 - 7 - 8 +- 9 From 3ec577126fb8b29d2784959dc33ec9b3a48623f3 Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Thu, 20 Feb 2025 07:58:25 +0000 Subject: [PATCH 19/20] =?UTF-8?q?test(api):=20add=20api=20tests=20--=20Cha?= =?UTF-8?q?pter=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/test_post_api-v1-schedules.ts | 283 ++++++------------ 1 file changed, 98 insertions(+), 185 deletions(-) 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 index 2b79bcdec2..ad64d67247 100644 --- 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 @@ -1,246 +1,159 @@ -import axios, { AxiosResponse } from 'axios'; +import axios, { AxiosError } from 'axios'; import { describe, it, expect, beforeAll } from '@jest/globals'; -/** - * Jest test suite for POST /api/v1/schedules - * Requirements: - * 1. Input Validation - * 2. Response Validation - * 3. Response Headers Validation - * 4. Edge Case & Limit Testing - * 5. Authorization & Authentication - * - * - Loads API base URL from process.env.API_BASE_URL - * - Loads auth token from process.env.API_AUTH_TOKEN - * - Uses axios as HTTP client - * - Written in TypeScript (with Jest as test framework) - * - Ensures Prettier style formatting - */ - describe('POST /api/v1/schedules', () => { - let baseURL: string; + let baseUrl: string; let authToken: string; beforeAll(() => { - baseURL = process.env.API_BASE_URL || 'http://localhost:3000'; + // Load environment variables + baseUrl = process.env.API_BASE_URL || 'http://localhost:3000'; authToken = process.env.API_AUTH_TOKEN || ''; }); - /** - * Helper function to create a schedule. - * Attaches Authorization header if provided. - */ - const createSchedule = async ( - payload: Record, - token?: string - ): Promise => { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - return axios.post(`${baseURL}/api/v1/schedules`, payload, { headers }); - }; - - /** - * TEST 1: Valid Payload (Happy Path) - * - * - Expects 200 response code ("Schedule created successfully"). - * - Verifies JSON response matches expected schema. - * - Checks response headers. - */ - it('should create a schedule with valid payload', async () => { - const validPayload = { - // Example payload conforming to #/components/schemas/ScheduleObject - // Adjust fields according to your actual schema. + it('should create a schedule with valid data (200)', async () => { + const payload = { type: 'IMPERATIVE', - title: 'Automated Test Schedule', - startTime: '2023-01-01T00:00:00Z', - endTime: '2023-01-01T01:00:00Z', - // Add other required fields as needed + name: 'Example schedule', + // Add other required fields here }; - const response = await createSchedule(validPayload, authToken); + const response = await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); - // Response Validation + // Response validation expect(response.status).toBe(200); expect(response.headers['content-type']).toContain('application/json'); - - // Sample checks against the response body schema - // Adjust these checks depending on your actual ScheduleObject schema. + // Validate response body (example checks) expect(response.data).toHaveProperty('id'); expect(response.data).toHaveProperty('type', 'IMPERATIVE'); - expect(response.data).toHaveProperty('title', 'Automated Test Schedule'); + // Add additional schema validations as needed }); - /** - * TEST 2: Missing Required Fields => 400 or 422 - * - * - Expects error code 400 or 422 when payload is invalid. - */ - it('should return 400 or 422 for invalid payload (missing fields)', async () => { - const invalidPayload = { - // Omit required fields to trigger validation error + it('should return 400 or 422 if required fields are missing', async () => { + const payload = { + // Omitting a required field (e.g., name) type: 'IMPERATIVE', - // title missing, startTime missing, etc. }; try { - await createSchedule(invalidPayload, authToken); - // If request succeeds unexpectedly - fail('Expected request to fail for invalid payload, but it succeeded.'); - } catch (error: any) { - const status = error.response?.status; - // The API might respond with 400 or 422 for invalid payload. - expect([400, 422]).toContain(status); + await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); + fail('Request should not succeed with missing required fields'); + } catch (error) { + const axiosError = error as AxiosError; + expect([400, 422]).toContain(axiosError.response?.status); } }); - /** - * TEST 3: Wrong Data Types => 400 or 422 - * - * - Expects error code 400 or 422 when data types are incorrect. - */ - it('should return 400 or 422 for incorrect data types', async () => { - const invalidPayload = { + it('should return 400 or 422 if data type is invalid', async () => { + const payload = { type: 'IMPERATIVE', - title: 1234, // title should be a string - startTime: true, // startTime should be a string/timestamp, not boolean - endTime: null, // etc. + // Passing an invalid data type for the name field + name: 12345, }; try { - await createSchedule(invalidPayload, authToken); - fail('Expected request to fail for incorrect data types, but it succeeded.'); - } catch (error: any) { - const status = error.response?.status; - expect([400, 422]).toContain(status); + await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); + fail('Request should not succeed with invalid data types'); + } catch (error) { + const axiosError = error as AxiosError; + expect([400, 422]).toContain(axiosError.response?.status); } }); - /** - * TEST 4: Unauthorized Access => 401 or 403 - * - * - No token or invalid token should yield 401 or 403. - */ - it('should return 401 or 403 for unauthorized (missing or invalid token)', async () => { - const validPayload = { + it('should return 401 or 403 if unauthorized or forbidden', async () => { + const payload = { type: 'IMPERATIVE', - title: 'Unauthorized Test Schedule', - startTime: '2023-01-01T02:00:00Z', - endTime: '2023-01-01T03:00:00Z', + name: 'Unauthorized test', }; try { - // Call without token - await createSchedule(validPayload); - fail('Expected 401 or 403 for unauthorized access, but it succeeded.'); - } catch (error: any) { - const status = error.response?.status; - expect([401, 403]).toContain(status); - } - }); - - /** - * TEST 5: Empty Request Body => 400 or 422 - * - * - Expects the API to reject empty body. - */ - it('should return 400 or 422 when request body is empty', async () => { - try { - await createSchedule({}, authToken); - fail('Expected 400 or 422 for empty body, but request succeeded.'); - } catch (error: any) { - const status = error.response?.status; - expect([400, 422]).toContain(status); + await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + // Using an invalid token + Authorization: 'Bearer INVALID_TOKEN', + 'Content-Type': 'application/json', + }, + }); + fail('Request should not succeed with invalid token'); + } catch (error) { + const axiosError = error as AxiosError; + // Either 401 or 403 is acceptable + expect([401, 403]).toContain(axiosError.response?.status); } }); - /** - * TEST 6: Large Payload => check for 400, 413 (Payload Too Large), or success - * - * - Sends a large string to ensure the API handles or rejects appropriately. - */ - it('should handle large payload gracefully', async () => { - // Generate a large string. - const largeString = 'x'.repeat(10000); // 10k characters - - const largePayload = { + it('should handle a large payload without server error', async () => { + // Create a large string payload + const largeString = 'x'.repeat(10000); + const payload = { type: 'IMPERATIVE', - title: largeString, - startTime: '2023-01-02T00:00:00Z', - endTime: '2023-01-02T01:00:00Z', + name: largeString, }; - let responseStatus: number = 0; - try { - const response = await createSchedule(largePayload, authToken); - responseStatus = response.status; - } catch (error: any) { - responseStatus = error.response?.status; + const response = await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); + // Depending on API validation, may return 200 or 400/422 + expect([200, 400, 422]).toContain(response.status); + } catch (error) { + const axiosError = error as AxiosError; + // Some APIs may return 413 or 500 for unusually large payloads + expect([400, 413, 422, 500]).toContain(axiosError.response?.status); } - - // The API might accept it (200) or reject it (400, 413, 422, etc.). - // Adjust as appropriate for your API's expected behavior. - expect([200, 400, 413, 422]).toContain(responseStatus); }); - /** - * TEST 7: Endpoint Not Found => 404 - * - * - Calls a non-existent path to ensure we get a 404. - */ - it('should return 404 for invalid endpoint', async () => { + it('should return 400 or 422 if body is empty', async () => { + const payload = {}; + try { - await axios.post(`${baseURL}/api/v1/schedulezzz`, {}, { + await axios.post(`${baseUrl}/api/v1/schedules`, payload, { headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json', }, }); - fail('Expected 404 for invalid endpoint, but request succeeded.'); - } catch (error: any) { - expect(error.response?.status).toBe(404); + fail('Request should not succeed with empty body'); + } catch (error) { + const axiosError = error as AxiosError; + expect([400, 422]).toContain(axiosError.response?.status); } }); - /** - * TEST 8: Server Error Handling => 500 - * - * - This test is illustrative if your API might throw a 500 for certain conditions. - * Adjust to match a scenario that triggers a 500 in your environment. - */ - it('should handle 500 Internal Server Error (if applicable)', async () => { - // This scenario is highly dependent on your API's implementation. - // Example approach: pass in data that forces a server error. - - const erroneousPayload = { + it('should validate response headers for a valid request', async () => { + const payload = { type: 'IMPERATIVE', - title: 'Trigger Server Error', - // Suppose passing a specific field or invalid data triggers a 500. - forceServerError: true, + name: 'Header test schedule', }; - let statusCode: number | undefined; + const response = await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); - try { - await createSchedule(erroneousPayload, authToken); - } catch (error: any) { - statusCode = error.response?.status; - } - - // If the API can return 500 under certain conditions - // We check if that scenario can occur. - // Remove or adjust this test if 500 isn't expected. - if (statusCode === 500) { - expect(statusCode).toBe(500); - } else { - // If your API won't return 500 for such scenarios, you may want to adjust expectations. - expect([400, 422, 500]).toContain(statusCode); - } + expect(response.status).toBe(200); + // Validate Content-Type header + expect(response.headers['content-type']).toContain('application/json'); + // Add additional header checks as necessary }); -}); +}); \ No newline at end of file From 8bcdaf365c090d45a3abd9936f006aaf34997783 Mon Sep 17 00:00:00 2001 From: chapter-test-bot Date: Thu, 20 Feb 2025 07:58:26 +0000 Subject: [PATCH 20/20] =?UTF-8?q?ci(apitest):=20add=20Github=20Actions=20w?= =?UTF-8?q?orkflow=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