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" diff --git a/README.md b/README.md index 4e3416c4aa..ceca49c0ba 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,14 @@ To setup and develop locally or contribute to the open source project, follow ou + +## Test +- 1 +- 2 +- 3 +- 4 +- 5 +- 6 +- 7 +- 8 +- 9 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..ad64d67247 --- /dev/null +++ b/chapter_api_tests/2024-04/validation/test_post_api-v1-schedules.ts @@ -0,0 +1,159 @@ +import axios, { AxiosError } from 'axios'; +import { describe, it, expect, beforeAll } from '@jest/globals'; + +describe('POST /api/v1/schedules', () => { + 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 || ''; + }); + + it('should create a schedule with valid data (200)', async () => { + const payload = { + type: 'IMPERATIVE', + name: 'Example schedule', + // Add other required fields here + }; + + const response = await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); + + // Response validation + expect(response.status).toBe(200); + expect(response.headers['content-type']).toContain('application/json'); + // Validate response body (example checks) + expect(response.data).toHaveProperty('id'); + expect(response.data).toHaveProperty('type', 'IMPERATIVE'); + // Add additional schema validations as needed + }); + + it('should return 400 or 422 if required fields are missing', async () => { + const payload = { + // Omitting a required field (e.g., name) + type: 'IMPERATIVE', + }; + + try { + 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); + } + }); + + it('should return 400 or 422 if data type is invalid', async () => { + const payload = { + type: 'IMPERATIVE', + // Passing an invalid data type for the name field + name: 12345, + }; + + try { + 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); + } + }); + + it('should return 401 or 403 if unauthorized or forbidden', async () => { + const payload = { + type: 'IMPERATIVE', + name: 'Unauthorized test', + }; + + try { + 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); + } + }); + + it('should handle a large payload without server error', async () => { + // Create a large string payload + const largeString = 'x'.repeat(10000); + const payload = { + type: 'IMPERATIVE', + name: largeString, + }; + + try { + 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); + } + }); + + it('should return 400 or 422 if body is empty', async () => { + const payload = {}; + + try { + await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); + fail('Request should not succeed with empty body'); + } catch (error) { + const axiosError = error as AxiosError; + expect([400, 422]).toContain(axiosError.response?.status); + } + }); + + it('should validate response headers for a valid request', async () => { + const payload = { + type: 'IMPERATIVE', + name: 'Header test schedule', + }; + + const response = await axios.post(`${baseUrl}/api/v1/schedules`, payload, { + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + }); + + 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