├── .api ├── booking │ ├── GET_all_bookings_schema.json │ ├── GET_booking_id_schema.json │ ├── GET_booking_summary_schema.json │ ├── POST_booking_schema.json │ └── PUT_booking_id_schema.json ├── branding │ ├── GET_branding_schema.json │ └── PUT_branding_schema.json ├── message │ ├── GET_message_count_schema.json │ ├── GET_message_id_schema.json │ ├── GET_message_schema.json │ └── POST_message_schema.json ├── report │ ├── GET_report_room_id_schema.json │ └── GET_report_schema.json └── room │ ├── GET_room_id_schema.json │ ├── GET_room_schema.json │ ├── POST_room_schema.json │ └── PUT_room_id_schema.json ├── .env ├── .env.local ├── .env.staging ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── daily-full.yml │ ├── on-pr-files-changed.yml │ └── openai-pr-reviewer.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── bruno ├── auth │ ├── login.bru │ └── validate.bru ├── booking │ ├── GET booking id.bru │ ├── GET booking.bru │ ├── booking id.bru │ ├── booking id1.bru │ ├── booking summary.bru │ └── booking.bru ├── branding │ ├── PUT branding.bru │ └── branding.bru ├── bruno.json ├── environments │ ├── automationintesting.online.bru │ └── localhost.bru ├── message │ ├── DELETE message.bru │ ├── POST message.bru │ ├── PUT message read.bru │ ├── message count.bru │ ├── message id.bru │ └── message.bru ├── report │ ├── report room id.bru │ └── report.bru └── room │ ├── DELETE room.bru │ ├── GET room id.bru │ ├── GET room.bru │ ├── PUT room.bru │ └── room.bru ├── jest.config.js ├── lib ├── datafactory │ ├── auth.ts │ ├── booking.ts │ ├── message.ts │ └── room.ts ├── fixtures │ ├── fixtures.ts │ ├── toBeOneOfValues.ts │ ├── toBeValidDate.ts │ └── typesExpects.ts └── helpers │ ├── arrayFunctions.ts │ ├── branding.ts │ ├── capitalizeString.ts │ ├── coverage.ts │ ├── createAssertions.ts │ ├── createHeaders.ts │ ├── date.ts │ ├── env.ts │ ├── roomFeatures.ts │ ├── schemaData │ ├── Auth.ts │ ├── Booking.ts │ ├── Branding.ts │ ├── Message.ts │ ├── Report.ts │ └── Room.ts │ ├── schemaHelperFunctions.ts │ ├── tests │ ├── createAssertions.test.ts │ ├── createHeaders.test.ts │ ├── schemaHelperFunctions.integration.test.ts │ └── schemaHelperFunctions.test.ts │ ├── validateAgainstSchema.ts │ ├── validateJsonSchema.ts │ └── warnings.ts ├── package-lock.json ├── package.json ├── playwright.config.ts ├── tests ├── auth.setup.ts ├── auth │ ├── login.post.spec.ts │ ├── logout.post.spec.ts │ └── validate.post.spec.ts ├── booking │ ├── booking.delete.spec.ts │ ├── booking.get.spec.ts │ ├── booking.post.spec.ts │ └── booking.put.spec.ts ├── branding │ └── branding.spec.ts ├── completion.teardown.ts ├── coverage.setup.ts ├── message │ ├── message.delete.spec.ts │ ├── message.get.spec.ts │ ├── message.post.spec.ts │ └── message.put.spec.ts ├── report │ └── report.get.spec.ts ├── room │ ├── room.delete.spec.ts │ ├── room.get.spec.ts │ ├── room.post.spec.ts │ └── room.put.spec.ts └── test.spec.ts └── tsconfig.json /.api/booking/GET_all_bookings_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "bookings": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "bookingid": { 10 | "type": "integer" 11 | }, 12 | "roomid": { 13 | "type": "integer" 14 | }, 15 | "firstname": { 16 | "type": "string" 17 | }, 18 | "lastname": { 19 | "type": "string" 20 | }, 21 | "depositpaid": { 22 | "type": "boolean" 23 | }, 24 | "bookingdates": { 25 | "type": "object", 26 | "properties": { 27 | "checkin": { 28 | "type": "string" 29 | }, 30 | "checkout": { 31 | "type": "string" 32 | } 33 | }, 34 | "required": ["checkin", "checkout"] 35 | } 36 | }, 37 | "required": ["bookingid", "roomid", "firstname", "lastname", "depositpaid", "bookingdates"] 38 | } 39 | } 40 | }, 41 | "required": ["bookings"] 42 | } 43 | -------------------------------------------------------------------------------- /.api/booking/GET_booking_id_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "bookingid": { 5 | "type": "integer" 6 | }, 7 | "roomid": { 8 | "type": "integer" 9 | }, 10 | "firstname": { 11 | "type": "string" 12 | }, 13 | "lastname": { 14 | "type": "string" 15 | }, 16 | "depositpaid": { 17 | "type": "boolean" 18 | }, 19 | "bookingdates": { 20 | "type": "object", 21 | "properties": { 22 | "checkin": { 23 | "type": "string" 24 | }, 25 | "checkout": { 26 | "type": "string" 27 | } 28 | }, 29 | "required": ["checkin", "checkout"] 30 | } 31 | }, 32 | "required": ["bookingid", "roomid", "firstname", "lastname", "depositpaid", "bookingdates"] 33 | } 34 | -------------------------------------------------------------------------------- /.api/booking/GET_booking_summary_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "bookings": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "bookingDates": { 10 | "type": "object", 11 | "properties": { 12 | "checkin": { 13 | "type": "string" 14 | }, 15 | "checkout": { 16 | "type": "string" 17 | } 18 | }, 19 | "required": ["checkin", "checkout"] 20 | } 21 | }, 22 | "required": ["bookingDates"] 23 | } 24 | } 25 | }, 26 | "required": ["bookings"] 27 | } 28 | -------------------------------------------------------------------------------- /.api/booking/POST_booking_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "bookingid": { 5 | "type": "integer" 6 | }, 7 | "booking": { 8 | "type": "object", 9 | "properties": { 10 | "bookingid": { 11 | "type": "integer" 12 | }, 13 | "roomid": { 14 | "type": "integer" 15 | }, 16 | "firstname": { 17 | "type": "string" 18 | }, 19 | "lastname": { 20 | "type": "string" 21 | }, 22 | "depositpaid": { 23 | "type": "boolean" 24 | }, 25 | "bookingdates": { 26 | "type": "object", 27 | "properties": { 28 | "checkin": { 29 | "type": "string" 30 | }, 31 | "checkout": { 32 | "type": "string" 33 | } 34 | }, 35 | "required": ["checkin", "checkout"] 36 | } 37 | }, 38 | "required": ["bookingid", "roomid", "firstname", "lastname", "depositpaid", "bookingdates"] 39 | } 40 | }, 41 | "required": ["bookingid", "booking"] 42 | } 43 | -------------------------------------------------------------------------------- /.api/booking/PUT_booking_id_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "bookingid": { 5 | "type": "integer" 6 | }, 7 | "booking": { 8 | "type": "object", 9 | "properties": { 10 | "bookingid": { 11 | "type": "integer" 12 | }, 13 | "roomid": { 14 | "type": "integer" 15 | }, 16 | "firstname": { 17 | "type": "string" 18 | }, 19 | "lastname": { 20 | "type": "string" 21 | }, 22 | "depositpaid": { 23 | "type": "boolean" 24 | }, 25 | "bookingdates": { 26 | "type": "object", 27 | "properties": { 28 | "checkin": { 29 | "type": "string" 30 | }, 31 | "checkout": { 32 | "type": "string" 33 | } 34 | }, 35 | "required": ["checkin", "checkout"] 36 | } 37 | }, 38 | "required": ["bookingid", "roomid", "firstname", "lastname", "depositpaid", "bookingdates"] 39 | } 40 | }, 41 | "required": ["bookingid", "booking"] 42 | } 43 | -------------------------------------------------------------------------------- /.api/branding/GET_branding_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "name": { 5 | "type": "string" 6 | }, 7 | "map": { 8 | "type": "object", 9 | "properties": { 10 | "latitude": { 11 | "type": "number" 12 | }, 13 | "longitude": { 14 | "type": "number" 15 | } 16 | }, 17 | "required": ["latitude", "longitude"] 18 | }, 19 | "logoUrl": { 20 | "type": "string" 21 | }, 22 | "description": { 23 | "type": "string" 24 | }, 25 | "contact": { 26 | "type": "object", 27 | "properties": { 28 | "name": { 29 | "type": "string" 30 | }, 31 | "address": { 32 | "type": "string" 33 | }, 34 | "phone": { 35 | "type": "string" 36 | }, 37 | "email": { 38 | "type": "string" 39 | } 40 | }, 41 | "required": ["name", "address", "phone", "email"] 42 | } 43 | }, 44 | "required": ["name", "map", "logoUrl", "description", "contact"] 45 | } 46 | -------------------------------------------------------------------------------- /.api/branding/PUT_branding_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "name": { 5 | "type": "string" 6 | }, 7 | "map": { 8 | "type": "object", 9 | "properties": { 10 | "latitude": { 11 | "type": "number" 12 | }, 13 | "longitude": { 14 | "type": "number" 15 | } 16 | }, 17 | "required": ["latitude", "longitude"] 18 | }, 19 | "logoUrl": { 20 | "type": "string" 21 | }, 22 | "description": { 23 | "type": "string" 24 | }, 25 | "contact": { 26 | "type": "object", 27 | "properties": { 28 | "name": { 29 | "type": "string" 30 | }, 31 | "address": { 32 | "type": "string" 33 | }, 34 | "phone": { 35 | "type": "string" 36 | }, 37 | "email": { 38 | "type": "string" 39 | } 40 | }, 41 | "required": ["name", "address", "phone", "email"] 42 | } 43 | }, 44 | "required": ["name", "map", "logoUrl", "description", "contact"] 45 | } 46 | -------------------------------------------------------------------------------- /.api/message/GET_message_count_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "count": { 5 | "type": "integer" 6 | } 7 | }, 8 | "required": ["count"] 9 | } 10 | -------------------------------------------------------------------------------- /.api/message/GET_message_id_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "messageid": { 5 | "type": "integer" 6 | }, 7 | "name": { 8 | "type": "string" 9 | }, 10 | "email": { 11 | "type": "string" 12 | }, 13 | "phone": { 14 | "type": "string" 15 | }, 16 | "subject": { 17 | "type": "string" 18 | }, 19 | "description": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": ["messageid", "name", "email", "phone", "subject", "description"] 24 | } 25 | -------------------------------------------------------------------------------- /.api/message/GET_message_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "messages": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer" 11 | }, 12 | "name": { 13 | "type": "string" 14 | }, 15 | "subject": { 16 | "type": "string" 17 | }, 18 | "read": { 19 | "type": "boolean" 20 | } 21 | }, 22 | "required": ["id", "name", "subject", "read"] 23 | } 24 | } 25 | }, 26 | "required": ["messages"] 27 | } 28 | -------------------------------------------------------------------------------- /.api/message/POST_message_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "messageid": { 5 | "type": "integer" 6 | }, 7 | "name": { 8 | "type": "string" 9 | }, 10 | "email": { 11 | "type": "string" 12 | }, 13 | "phone": { 14 | "type": "string" 15 | }, 16 | "subject": { 17 | "type": "string" 18 | }, 19 | "description": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": ["messageid", "name", "email", "phone", "subject", "description"] 24 | } 25 | -------------------------------------------------------------------------------- /.api/report/GET_report_room_id_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "report": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "start": { 10 | "type": "string" 11 | }, 12 | "end": { 13 | "type": "string" 14 | }, 15 | "title": { 16 | "type": "string" 17 | } 18 | }, 19 | "required": ["start", "end", "title"] 20 | } 21 | } 22 | }, 23 | "required": ["report"] 24 | } 25 | -------------------------------------------------------------------------------- /.api/report/GET_report_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "report": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "start": { 10 | "type": "string" 11 | }, 12 | "end": { 13 | "type": "string" 14 | }, 15 | "title": { 16 | "type": "string" 17 | } 18 | }, 19 | "required": ["start", "end", "title"] 20 | } 21 | } 22 | }, 23 | "required": ["report"] 24 | } 25 | -------------------------------------------------------------------------------- /.api/room/GET_room_id_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "roomid": { 5 | "type": "integer" 6 | }, 7 | "roomName": { 8 | "type": "string" 9 | }, 10 | "type": { 11 | "type": "string" 12 | }, 13 | "accessible": { 14 | "type": "boolean" 15 | }, 16 | "image": { 17 | "type": "string" 18 | }, 19 | "description": { 20 | "type": "string" 21 | }, 22 | "features": { 23 | "type": "array", 24 | "items": { 25 | "type": "string" 26 | } 27 | }, 28 | "roomPrice": { 29 | "type": "integer" 30 | } 31 | }, 32 | "required": ["roomid", "roomName", "type", "accessible", "image", "description", "features", "roomPrice"] 33 | } 34 | -------------------------------------------------------------------------------- /.api/room/GET_room_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "rooms": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "roomid": { 10 | "type": "integer" 11 | }, 12 | "roomName": { 13 | "type": "string" 14 | }, 15 | "type": { 16 | "type": "string" 17 | }, 18 | "accessible": { 19 | "type": "boolean" 20 | }, 21 | "image": { 22 | "type": "string" 23 | }, 24 | "description": { 25 | "type": "string" 26 | }, 27 | "features": { 28 | "type": "array", 29 | "items": { 30 | "type": "string" 31 | } 32 | }, 33 | "roomPrice": { 34 | "type": "integer" 35 | } 36 | }, 37 | "required": ["roomid", "roomName", "type", "accessible", "image", "description", "features", "roomPrice"] 38 | } 39 | } 40 | }, 41 | "required": ["rooms"] 42 | } 43 | -------------------------------------------------------------------------------- /.api/room/POST_room_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "roomid": { 5 | "type": "integer" 6 | }, 7 | "roomName": { 8 | "type": "string" 9 | }, 10 | "type": { 11 | "type": "string" 12 | }, 13 | "accessible": { 14 | "type": "boolean" 15 | }, 16 | "image": { 17 | "type": "string" 18 | }, 19 | "description": { 20 | "type": "string" 21 | }, 22 | "features": { 23 | "type": "array", 24 | "items": { 25 | "type": "string" 26 | } 27 | }, 28 | "roomPrice": { 29 | "type": "integer" 30 | } 31 | }, 32 | "required": ["roomid", "roomName", "type", "accessible", "image", "description", "features", "roomPrice"] 33 | } 34 | -------------------------------------------------------------------------------- /.api/room/PUT_room_id_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "roomid": { 5 | "type": "integer" 6 | }, 7 | "roomName": { 8 | "type": "string" 9 | }, 10 | "type": { 11 | "type": "string" 12 | }, 13 | "accessible": { 14 | "type": "boolean" 15 | }, 16 | "image": { 17 | "type": "string" 18 | }, 19 | "description": { 20 | "type": "string" 21 | }, 22 | "features": { 23 | "type": "array", 24 | "items": { 25 | "type": "string" 26 | } 27 | }, 28 | "roomPrice": { 29 | "type": "integer" 30 | } 31 | }, 32 | "required": ["roomid", "roomName", "type", "accessible", "image", "description", "features", "roomPrice"] 33 | } 34 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | URL=https://automationintesting.online/ 2 | ADMIN_NAME=admin 3 | ADMIN_PASSWORD=password 4 | SECRET_API_KEY=secret 5 | 6 | # Envronment variables for Currents.dev Reporter 7 | # CURRENTS_PROJECT_ID= 8 | # CURRENTS_RECORD_KEY= 9 | # CURRENTS_CI_BUILD_ID= -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | URL=http://localhost/ 2 | ADMIN_NAME=admin 3 | ADMIN_PASSWORD=password 4 | SECRET_API_KEY=secret -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | URL=https://staging-automationintesting.online/ 2 | ADMIN_NAME=admin 3 | ADMIN_PASSWORD=password 4 | SECRET_API_KEY=secret -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Playwright Specific 2 | node_modules/ 3 | test-results/ 4 | playwright-report 5 | summary.json 6 | 7 | # IDE - VSCode 8 | .vscode/* 9 | 10 | # System Files 11 | .DS_Store 12 | Thumbs.db 13 | {"mode":"full","isActive":false} 14 | 15 | # Docs files 16 | *_spec3.json 17 | 18 | # Warnings file 19 | warnings.log -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:@typescript-eslint/stylistic", 7 | "prettier", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | plugins: ["@typescript-eslint"], 11 | root: true, 12 | rules: { 13 | "@typescript-eslint/no-explicit-any": 0, 14 | "@typescript-eslint/no-floating-promises": 0, 15 | "no-console": 0, 16 | "no-restricted-syntax": [ 17 | "error", 18 | { 19 | selector: "CallExpression[callee.property.name='only']", 20 | message: "We don't want to leave .only on our tests😱", 21 | }, 22 | { 23 | selector: "CallExpression[callee.name='validateJsonSchema'][arguments.length!=3]", 24 | message: "We don't want to commit validateJsonSchema(*,*,*,true)😎", 25 | }, 26 | ], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.github/workflows/daily-full.yml: -------------------------------------------------------------------------------- 1 | name: Daily Playwright API Checks 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | - cron: "0 6 * * *" 7 | workflow_dispatch: 8 | inputs: 9 | base_url: 10 | description: "URL, to run tests against" 11 | required: true 12 | default: https://automationintesting.online/ 13 | permissions: 14 | contents: write 15 | pages: write 16 | id-token: write 17 | 18 | jobs: 19 | playwright-automation-checks: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | timeout-minutes: 10 24 | runs-on: ubuntu-latest 25 | 26 | env: 27 | BASE_URL: ${{ github.event.inputs.base_url }} 28 | CURRENTS_PROJECT_ID: ${{ secrets.CURRENTS_PROJECT_ID }} 29 | CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | with: 34 | fetch-depth: 0 35 | 36 | - uses: actions/setup-node@v3 37 | with: 38 | node-version: 16 39 | 40 | - name: Cache node_modules 41 | uses: actions/cache@v3 42 | id: node-modules-cache 43 | with: 44 | path: | 45 | node_modules 46 | key: modules-${{ hashFiles('package-lock.json') }} 47 | - run: npm ci --ignore-scripts 48 | if: steps.node-modules-cache.outputs.cache-hit != 'true' 49 | 50 | - name: Get installed Playwright version 51 | id: playwright-version 52 | run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package-lock.json').dependencies['@playwright/test'].version)")" >> $GITHUB_ENV 53 | - name: Cache playwright binaries 54 | uses: actions/cache@v3 55 | id: playwright-cache 56 | with: 57 | path: | 58 | ~/.cache/ms-playwright 59 | key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} 60 | - run: npx playwright install --with-deps 61 | if: steps.playwright-cache.outputs.cache-hit != 'true' 62 | - run: npx playwright install-deps 63 | if: steps.playwright-cache.outputs.cache-hit != 'true' 64 | 65 | - name: Run lint 66 | run: npm run lint 67 | - name: Run prettier 68 | run: npm run prettier 69 | # - name: Run UnitTests 70 | # run: npm run ut 71 | 72 | - name: Set BASE_URL if not passed in 73 | if: env.BASE_URL == null 74 | run: | 75 | echo "BASE_URL=https://automationintesting.online/" >> $GITHUB_ENV 76 | 77 | - name: Run Playwright tests 78 | env: 79 | CURRENTS_PROJECT_ID: ${{ secrets.CURRENTS_PROJECT_ID }} 80 | CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} 81 | CURRENTS_CI_BUILD_ID: reporter-${{ github.repository }}-${{ github.run_id }}-${{ github.run_attempt }} 82 | URL: ${{ env.BASE_URL}} 83 | run: | 84 | echo "The github event is: ${{ github.event_name }}" 85 | npm run test 86 | 87 | # The following steps are for deploying the report to GitHub Pages 88 | - name: Setup Pages 89 | if: always() 90 | uses: actions/configure-pages@v3 91 | 92 | - uses: actions/upload-artifact@v4 93 | if: always() 94 | with: 95 | name: report-artifact 96 | path: playwright-report/ 97 | retention-days: 3 98 | 99 | - name: Upload Pages Artifact 100 | uses: actions/upload-pages-artifact@v3 101 | if: always() 102 | 103 | with: 104 | path: "playwright-report/" 105 | 106 | - name: Deploy to GitHub Pages 107 | if: always() 108 | id: deployment 109 | uses: actions/deploy-pages@v4 110 | -------------------------------------------------------------------------------- /.github/workflows/on-pr-files-changed.yml: -------------------------------------------------------------------------------- 1 | name: Changed Files Playwright API Checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | permissions: 8 | contents: write 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | playwright-automation-checks: 14 | environment: 15 | name: github-pages 16 | url: ${{ steps.deployment.outputs.page_url }} 17 | timeout-minutes: 10 18 | runs-on: ubuntu-latest 19 | 20 | env: 21 | BASE_URL: ${{ github.event.inputs.base_url }} 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 16 31 | 32 | - name: Cache node_modules 33 | uses: actions/cache@v3 34 | id: node-modules-cache 35 | with: 36 | path: | 37 | node_modules 38 | key: modules-${{ hashFiles('package-lock.json') }} 39 | - run: npm ci --ignore-scripts 40 | if: steps.node-modules-cache.outputs.cache-hit != 'true' 41 | 42 | - name: Get installed Playwright version 43 | id: playwright-version 44 | run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package-lock.json').dependencies['@playwright/test'].version)")" >> $GITHUB_ENV 45 | - name: Cache playwright binaries 46 | uses: actions/cache@v3 47 | id: playwright-cache 48 | with: 49 | path: | 50 | ~/.cache/ms-playwright 51 | key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} 52 | - run: npx playwright install --with-deps 53 | if: steps.playwright-cache.outputs.cache-hit != 'true' 54 | - run: npx playwright install-deps 55 | if: steps.playwright-cache.outputs.cache-hit != 'true' 56 | 57 | - name: Run lint 58 | run: npm run lint 59 | - name: Run prettier 60 | run: npm run prettier 61 | 62 | - name: Set BASE_URL if not passed in 63 | if: env.BASE_URL == null 64 | run: | 65 | echo "BASE_URL=https://automationintesting.online/" >> $GITHUB_ENV 66 | 67 | - name: Run Playwright tests 68 | run: | 69 | echo "The github event is: ${{ github.event_name }}" 70 | URL=${{ env.BASE_URL }} npx playwright test --grep-invert @unsatisfactory --reporter=github --workers=1 --only-changed=origin/$GITHUB_BASE_REF 71 | -------------------------------------------------------------------------------- /.github/workflows/openai-pr-reviewer.yml: -------------------------------------------------------------------------------- 1 | name: Code Review 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | 7 | on: 8 | pull_request: 9 | pull_request_review_comment: 10 | types: [created] 11 | 12 | concurrency: 13 | group: ${{ github.repository }}-${{ github.event.number || github.head_ref || 14 | github.sha }}-${{ github.workflow }}-${{ github.event_name == 15 | 'pull_request_review_comment' && 'pr_comment' || 'pr' }} 16 | cancel-in-progress: ${{ github.event_name != 'pull_request_review_comment' }} 17 | 18 | jobs: 19 | review: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: coderabbitai/ai-pr-reviewer@latest 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 26 | with: 27 | debug: false 28 | review_simple_changes: false 29 | review_comment_lgtm: false 30 | openai_light_model: "gpt-3.5-turbo" 31 | openai_heavy_model: "gpt-3.5-turbo" 32 | summarize_release_notes: | 33 | Craft concise release notes for the pull request. 34 | Focus on the purpose and user impact, categorizing changes as "New Feature", "Bug Fix", 35 | "Documentation", "Refactor", "Style", "Test", "Chore", or "Revert". Provide a bullet-point list, 36 | e.g., "- New Feature: Added search functionality to the UI". Limit your response to 50-100 words 37 | and emphasize features visible to the end-user while omitting code-level details. You should end 38 | every summary with a funny pun or dad joke based on the code that you reviewed. 39 | There is no need to call it a dad joke or put just say it. An example of a 40 | dad joke is "I'm afraid for the calendar. Its days are numbered." 41 | 42 | system_message: | 43 | You are `@coderabbitai` (aka `github-actions[bot]`), a language model 44 | trained by OpenAI. Your purpose is to act as a highly experienced 45 | professional with focus on cloud-native. You should use slang words that 46 | milenials use such as yeet, lit, bruh, fam, etc. You should also use emojis. 47 | When you do respond, you should respond as arrogant 48 | and snarkey letting the developer know that they are not as good as 49 | you are. You should also be very opinionated and not afraid to share 50 | your opinion. You should also be very critical of the developer's 51 | code, and not let them off easy when they make an error. Whenever there 52 | is a chance to make a joke or pun, you should do so. 53 | 54 | 55 | Company context - 56 | CodeRabbit is an AI-powered Code reviewer.It boosts code quality and cuts manual effort. 57 | Offers context-aware, line-by-line feedback, highlights critical changes, 58 | enables bot interaction, and lets you commit suggestions directly from GitHub. 59 | 60 | When reviewing or generating content focus on key areas such as - 61 | - Accuracy 62 | - Relevance 63 | - Clarity 64 | - Technical depth 65 | - Call-to-action 66 | - SEO optimization 67 | - Brand consistency 68 | - Grammar and prose 69 | - Typos 70 | - Hyperlink suggestions 71 | - Graphics or images (suggest Dall-E image prompts if needed) 72 | - Empathy 73 | - Engagement 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /playwright/.cache/ 5 | .auth/ 6 | *_spec3.json 7 | 8 | # System Files 9 | .DS_Store 10 | Thumbs.db 11 | 12 | # Warnings file 13 | warnings.log -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run prettier 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Playwright Specific 2 | node_modules/ 3 | test-results/ 4 | playwright-report 5 | summary.json 6 | 7 | # IDE - VSCode 8 | .vscode/* 9 | 10 | # System Files 11 | .DS_Store 12 | Thumbs.db 13 | {"mode":"full","isActive":false} 14 | 15 | # Docs files 16 | *_spec3.json 17 | 18 | # Warnings file 19 | warnings.log 20 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": false, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "ms-playwright.playwright", 8 | "esbenp.prettier-vscode", 9 | "dbaeumer.vscode-eslint", 10 | "streetsidesoftware.code-spell-checker" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript", "typescript"], 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Playwright Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # playwright-api-test-demo 2 | 3 | This repository will serve as a place where I add API test Automation checks for articles written at 4 | 5 | ## Useful Swagger Links 6 | 7 | - [Auth Swagger UI](https://automationintesting.online/auth/swagger-ui/index.html#/) 8 | - [Booking Swagger UI](https://automationintesting.online/booking/swagger-ui/index.html#/) 9 | - [Room Swagger UI](https://automationintesting.online/room/swagger-ui/index.html#/) 10 | - [Branding Swagger UI](https://automationintesting.online/branding/swagger-ui/index.html#/) 11 | - [Report Swagger UI](https://automationintesting.online/report/swagger-ui/index.html#/) 12 | - [Message Swagger UI](https://automationintesting.online/message/swagger-ui/index.html#/) 13 | 14 | ## Contributing to playwright-api-test-demo 15 | 16 | ### Husky, ESLint, and Prettier 17 | 18 | We use a mix of [Husky](https://github.com/typicode/husky), [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) within our repository to help enforce consistent coding practices. Husky is a tool that will install a pre-commit hook to run the linter any time before you attempt to make a commit. This replaces the old pre-commit hook that was used before. to install the pre-commit hook you will need to run 19 | 20 | ```bash 21 | npm run prepare 22 | ``` 23 | 24 | ```bash 25 | npx husky install 26 | ``` 27 | 28 | You shouldn't have to run this command but for reference, the command used to generate the pre-commit file was 29 | 30 | ```bash 31 | npx husky add .husky/pre-commit "npm run lint && npm run prettier" 32 | ``` 33 | 34 | You are still able to bypass the commit hook by passing in --no-verify to your git commit message if needed. 35 | ESLint is a popular javascript/typescript linting tool. The configuration for ESLint can be found in `.eslintrc.cjs` file. Prettier, helps with styling (spaces, tabs, quotes, etc), config found in `.prettierrc`. 36 | 37 | ### Json Schema 38 | 39 | We generate json schemas with a `POST, PUT, PATCH and GET` test but not with a delete. To generate a json schema. An example of a test that generates a schema is below. It's best to follow the similar naming conventions 40 | 41 | ```javascript 42 | // Creates a snapshot of the schema and save to .api/booking/POST_booking_schema.json 43 | await validateJsonSchema("POST_booking", "booking", body, true); 44 | 45 | // Asserts that the body matches the snapshot found at .api/booking/POST_booking_schema.json 46 | await validateJsonSchema("POST_booking", "booking", body); 47 | ``` 48 | 49 | Example of how this is used in a test: 50 | 51 | ```javascript 52 | import { test, expect } from "@playwright/test"; 53 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 54 | 55 | test.describe("booking/ POST requests", async () => { 56 | test("POST new booking with full body", async ({ request }) => { 57 | const response = await request.post("booking/", { 58 | data: requestBody, 59 | }); 60 | 61 | expect(response.status()).toBe(201); 62 | const body = await response.json(); 63 | await validateJsonSchema("POST_booking", "booking", body); 64 | }); 65 | }); 66 | ``` 67 | 68 | - To make sure that response body schema is what we should expect based on our documentation we use `validateAgainstSchema()` function which takes the following parameters: 69 | 70 | - `Definition:` Validates an **object** against a specified **schema object** 71 | - object - The object to check schemaObject against. 72 | - schemaObject - The schema object name as documented in \*\_spec3.json. 73 | - docs - The type of docs (e.g. `public`, `internal` or `admin`). 74 | - notReturnedButInSchema - Any defined properties in schema but not returned by Falcon. 75 | - extraParamsReturned - Any undefined properties returned by Falcon; **_create a bug if there are any._** 76 | 77 | ```bash 78 | await validateAgainstSchema(body.user, "User", "public", [ 79 | "verification_code"]); 80 | ``` 81 | 82 | This function will validate the object from response (1st param) against the object (2nd param) from the documentation (3rd param), ignoring the fact that docs have more parameters defined (4th params). 83 | It will count keys in the returned and docs objects and validate that they have the same names. IF there is a mismatch -> there is going to be a failure on `.toEqual` since we validating keys from response and docs objects placing them in sorted arrays 84 | 85 | ```bash 86 | // compare object keys (need to be sorted since order differs) 87 | expect(docsObjectKeys.sort()).toEqual(responseObjectKeys.sort()); 88 | ``` 89 | 90 | The only thing which is not achieved with this approach is the fact there is no way for us to know if a new parameter is added. We have to track on our side how many keys each object has. 91 | 92 | `./lib/helpers/schemaData${docs}` which store objects and their parameter counts. 93 | When `validateAgainstSchema()` function finishes comparison of response and doc objects we then check docs object keys count vs what we store for that object. If there is a mismatch - a warning will be triggered at the end of run. Notice: test is not failing and just adds a warning which will give you directions what test needs to be updated (since there are possibly new params) and that you need to update our tracking files. 94 | 95 | To update tracking files you need to run any test with `GENERATE_SCHEMA_TRACKING_DATA=true`. It will overwrite existing 3 files BUT it's you who have to commit and push them to our repo. 96 | -------------------------------------------------------------------------------- /bruno/auth/login.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: login 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: https://automationintesting.online/auth/login 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | Accept: */* 15 | } 16 | 17 | body:json { 18 | { 19 | "username": "{{username}}", 20 | "password": "{{password}}" 21 | } 22 | 23 | } 24 | 25 | script:post-response { 26 | let headers = res.getHeaders(); 27 | console.log(headers) 28 | let cookies = headers["set-cookie"] 29 | const tokenValue = cookies[0].replace(/[\[\]']/g, '').split('; ').find(pair => pair.startsWith('token=')).split('=')[1]; 30 | 31 | console.log(cookies) 32 | console.log(tokenValue) 33 | 34 | bru.setEnvVar("token", tokenValue); 35 | } 36 | -------------------------------------------------------------------------------- /bruno/auth/validate.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: validate 3 | type: http 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: https://automationintesting.online/auth/validate 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "token": "{{token}}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bruno/booking/GET booking id.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: GET booking id 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/booking/ 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | -------------------------------------------------------------------------------- /bruno/booking/GET booking.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: GET booking 3 | type: http 4 | seq: 5 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/booking/1 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | -------------------------------------------------------------------------------- /bruno/booking/booking id.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: booking id 3 | type: http 4 | seq: 2 5 | } 6 | 7 | put { 8 | url: {{baseUrl}}/booking/2 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "bookingid": 2, 20 | "roomid": 1, 21 | "firstname": "Testy", 22 | "lastname": "McTesterSon", 23 | "depositpaid": false, 24 | "email": "newemail@testymctesterson.com", 25 | "phone": "212222345678", 26 | "bookingdates": { 27 | "checkin": "2023-06-10", 28 | "checkout": "2023-06-11" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bruno/booking/booking id1.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: booking id1 3 | type: http 4 | seq: 3 5 | } 6 | 7 | delete { 8 | url: {{baseUrl}}/booking/2 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "bookingid": 2, 20 | "roomid": 1, 21 | "firstname": "Testy", 22 | "lastname": "McTesterSon", 23 | "depositpaid": false, 24 | "email": "newemail@testymctesterson.com", 25 | "phone": "212222345678", 26 | "bookingdates": { 27 | "checkin": "2023-06-10", 28 | "checkout": "2023-06-11" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bruno/booking/booking summary.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: booking summary 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/booking/summary?roomid=1 9 | body: json 10 | auth: none 11 | } 12 | 13 | query { 14 | roomid: 1 15 | } 16 | -------------------------------------------------------------------------------- /bruno/booking/booking.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: booking 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{baseUrl}}/booking/ 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "bookingid": 3, 16 | "roomid": 1, 17 | "firstname": "Testy", 18 | "lastname": "McTesterSon", 19 | "depositpaid": true, 20 | "email": "testy@testymctesterson.com", 21 | "phone": "212222345678", 22 | "bookingdates": { 23 | "checkin": "2023-05-10", 24 | "checkout": "2023-05-11" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bruno/branding/PUT branding.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: PUT branding 3 | type: http 4 | seq: 2 5 | } 6 | 7 | put { 8 | url: {{baseUrl}}/branding/ 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "name": "Shady Meadows B&B", 20 | "map": { 21 | "latitude": 52.6351204, 22 | "longitude": 1.2733774 23 | }, 24 | "logoUrl": "https://automationintesting.online/images/rbp-logo.jpg", 25 | "description": "Welcome to Shady Meadows, a delightful Bed & Breakfast nestled in the hills on Newingtonfordburyshire. A place so beautiful you will never want to leave. All our rooms have comfortable beds and we provide breakfast from the locally sourced supermarket. It is a delightful place.", 26 | "contact": { 27 | "name": "Shady Meadows B&B", 28 | "address": "The Old Farmhouse, Shady Street, Newfordburyshire, NE1 410S", 29 | "phone": "012345678901", 30 | "email": "fake@fakeemail.com" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bruno/branding/branding.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: branding 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/branding/ 9 | body: json 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /bruno/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "automationintesting.online", 4 | "type": "collection" 5 | } 6 | -------------------------------------------------------------------------------- /bruno/environments/automationintesting.online.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | token: 3 | username: admin 4 | password: password 5 | baseUrl: https://automationintesting.online 6 | } 7 | -------------------------------------------------------------------------------- /bruno/environments/localhost.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | token: 3 | username: admin 4 | password: password 5 | baseUrl: http://localhost 6 | } 7 | -------------------------------------------------------------------------------- /bruno/message/DELETE message.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: DELETE message 3 | type: http 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: {{baseUrl}}/message/ 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "name": "string", 20 | "email": "test@test.com", 21 | "phone": "stringstrin", 22 | "subject": "string", 23 | "description": "stringstringstringst" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bruno/message/POST message.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: POST message 3 | type: http 4 | seq: 2 5 | } 6 | 7 | put { 8 | url: {{baseUrl}}/message/1/read 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "name": "string", 20 | "email": "test@test.com", 21 | "phone": "stringstrin", 22 | "subject": "string", 23 | "description": "stringstringstringst" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bruno/message/PUT message read.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: PUT message read 3 | type: http 4 | seq: 3 5 | } 6 | 7 | delete { 8 | url: {{baseUrl}}/message/1 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | -------------------------------------------------------------------------------- /bruno/message/message count.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: message count 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/message/count 9 | body: json 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /bruno/message/message id.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: message id 3 | type: http 4 | seq: 5 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/message/1 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | -------------------------------------------------------------------------------- /bruno/message/message.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: message 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/message/ 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | -------------------------------------------------------------------------------- /bruno/report/report room id.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: report room id 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/report/room/1 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | -------------------------------------------------------------------------------- /bruno/report/report.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: report 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/report/ 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | -------------------------------------------------------------------------------- /bruno/room/DELETE room.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: DELETE room 3 | type: http 4 | seq: 5 5 | } 6 | 7 | delete { 8 | url: {{baseUrl}}/room/2 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "roomid": 2, 20 | "roomName": "203", 21 | "type": "Suite", 22 | "accessible": true, 23 | "image": "string", 24 | "description": "This is a Description", 25 | "features": [ 26 | "TV", "FruitCake" 27 | ], 28 | "roomPrice": 201 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bruno/room/GET room id.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: GET room id 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/room/ 9 | body: json 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /bruno/room/GET room.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: GET room 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: {{baseUrl}}/room/2 9 | body: json 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /bruno/room/PUT room.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: PUT room 3 | type: http 4 | seq: 2 5 | } 6 | 7 | put { 8 | url: {{baseUrl}}/room/2 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "roomid": 2, 20 | "roomName": "203", 21 | "type": "Suite", 22 | "accessible": true, 23 | "image": "string", 24 | "description": "This is a Description", 25 | "features": [ 26 | "TV", "FruitCake" 27 | ], 28 | "roomPrice": 201 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bruno/room/room.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: room 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{baseUrl}}/room/ 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | cookie: token={{token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "roomid": 0, 20 | "roomName": "202", 21 | "type": "Suite", 22 | "accessible": true, 23 | "image": "string", 24 | "description": "This is a Description", 25 | "features": [ 26 | "TV", "FruitCake" 27 | ], 28 | "roomPrice": 201 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /* eslint-disable no-undef */ 3 | const { pathsToModuleNameMapper } = require("ts-jest"); 4 | const { compilerOptions } = require("./tsconfig"); 5 | 6 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 7 | module.exports = { 8 | preset: "ts-jest", 9 | //Below lines are added to make it possible to use paths from the tsconfig.json file in UTs 10 | modulePaths: [compilerOptions.baseUrl], 11 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), 12 | roots: ["lib"], 13 | }; 14 | -------------------------------------------------------------------------------- /lib/datafactory/auth.ts: -------------------------------------------------------------------------------- 1 | import Env from "@helpers/env"; 2 | import { expect, request } from "@playwright/test"; 3 | 4 | const url = Env.URL || "https://automationintesting.online/"; 5 | let cookies; 6 | 7 | /** 8 | * Returns valid cookies for the given username and password. 9 | * If a username and password aren't provided "admin" and "password" will be used 10 | * 11 | * @example 12 | * import { createCookies } from "../datafactory/auth"; 13 | * 14 | * const cookies = createCookies("Happy", "Mcpassword") 15 | * 16 | * const response = await request.put(`booking/${bookingId}`, { 17 | headers: { cookie: cookies }, 18 | data: body, 19 | }); 20 | */ 21 | export async function createCookies(username?: string, password?: string) { 22 | if (!username) { 23 | username = "admin"; 24 | } 25 | if (!password) { 26 | password = "password"; 27 | } 28 | 29 | const contextRequest = await request.newContext(); 30 | const response = await contextRequest.post(url + "auth/login", { 31 | data: { 32 | username: username, 33 | password: password, 34 | }, 35 | }); 36 | 37 | expect(response.status()).toBe(200); 38 | const headers = response.headers(); 39 | cookies = headers["set-cookie"]; 40 | return cookies; 41 | } 42 | 43 | /** 44 | * Returns valid token for the given username and password. 45 | * If a username and password aren't provided "admin" and "password" will be used 46 | * 47 | * @example 48 | * import { createToken } from "../datafactory/auth"; 49 | * 50 | * const token = createToken("Happy", "Mcpassword") 51 | * 52 | * const response = await request.post("auth/validate", { 53 | data: { token: token }, 54 | }); 55 | */ 56 | export async function createToken(username?: string, password?: string) { 57 | if (!username) { 58 | username = "admin"; 59 | } 60 | if (!password) { 61 | password = "password"; 62 | } 63 | 64 | const contextRequest = await request.newContext(); 65 | const response = await contextRequest.post(url + "auth/login", { 66 | data: { 67 | username: username, 68 | password: password, 69 | }, 70 | }); 71 | 72 | expect(response.status()).toBe(200); 73 | const headers = response.headers(); 74 | const tokenString = headers["set-cookie"].split(";")[0]; 75 | const token = tokenString.split("=")[1]; 76 | return token; 77 | } 78 | -------------------------------------------------------------------------------- /lib/datafactory/booking.ts: -------------------------------------------------------------------------------- 1 | import { expect, request } from "@playwright/test"; 2 | import { stringDateByDays } from "../helpers/date"; 3 | import { faker } from "@faker-js/faker"; 4 | import { createHeaders } from "../helpers/createHeaders"; 5 | import Env from "@helpers/env"; 6 | 7 | const url = Env.URL || "https://automationintesting.online/"; 8 | let bookingBody; 9 | let checkOutArray; 10 | 11 | export async function createRandomBookingBody(roomId: number, checkInString: string, checkOutString: string) { 12 | const bookingBody = { 13 | roomid: roomId, 14 | firstname: faker.person.firstName(), 15 | lastname: faker.person.lastName(), 16 | depositpaid: Math.random() < 0.5, //returns true or false 17 | email: faker.internet.email(), 18 | phone: faker.string.numeric(11), 19 | bookingdates: { 20 | checkin: checkInString, 21 | checkout: checkOutString, 22 | }, 23 | }; 24 | return bookingBody; 25 | } 26 | 27 | /** 28 | * This function will create a booking with provided roomId and a checkinDate 29 | * A checkout date will be randomly generated between 1 and 4 days after the checkinDate 30 | * 31 | * @param roomId: number for the room to create a booking for 32 | * @returns the body of the booking just created 33 | * 34 | * This code is wrapped in an assert retry details can be found 35 | * https://playwright.dev/docs/test-assertions#retrying 36 | */ 37 | export async function createFutureBooking(roomId: number) { 38 | let body; 39 | await expect(async () => { 40 | const headers = await createHeaders(); 41 | 42 | const futureCheckinDate = await futureOpenCheckinDate(roomId); 43 | const randBookingLength = faker.number.int({ min: 1, max: 4 }); 44 | 45 | const checkInString = futureCheckinDate.toISOString().split("T")[0]; 46 | const checkOutString = stringDateByDays(futureCheckinDate, randBookingLength); 47 | 48 | // console.log("booking length: " + randBookingLength); 49 | // console.log("checkin string: " + checkInString); 50 | // console.log("checkout string: " + checkOutString); 51 | 52 | bookingBody = { 53 | roomid: roomId, 54 | firstname: faker.person.firstName(), 55 | lastname: faker.person.lastName(), 56 | depositpaid: Math.random() < 0.5, //returns true or false 57 | email: faker.internet.email(), 58 | phone: faker.string.numeric(11), 59 | bookingdates: { 60 | checkin: checkInString, 61 | checkout: checkOutString, 62 | }, 63 | }; 64 | 65 | const createRequestContext = await request.newContext(); 66 | const response = await createRequestContext.post(url + "booking/", { 67 | headers: headers, 68 | data: bookingBody, 69 | }); 70 | 71 | expect(response.status()).toBe(201); 72 | body = await response.json(); 73 | }).toPass({ 74 | intervals: [1_000, 2_000, 5_000], 75 | timeout: 20_000, 76 | }); 77 | 78 | return body; 79 | } 80 | 81 | /** 82 | * This function will return all the bookings for a roomId 83 | * 84 | * @param roomId: number for the room you want to get the bookings for 85 | * @returns the body of the bookings for the room 86 | */ 87 | export async function getBookings(roomId: number) { 88 | const headers = await createHeaders(); 89 | 90 | const createRequestContext = await request.newContext(); 91 | const response = await createRequestContext.get(url + "booking/?roomid=" + roomId, { 92 | headers: headers, 93 | }); 94 | 95 | expect(response.status()).toBe(200); 96 | const body = await response.json(); 97 | // console.log(JSON.stringify(body)); 98 | return body; 99 | } 100 | 101 | /** 102 | * 103 | * @param bookingId: number for the booking you want to see the summary of 104 | * @returns the body of the booking/summary?roomid=${bookingId} endpoint 105 | */ 106 | export async function getBookingSummary(bookingId: number) { 107 | const createRequestContext = await request.newContext(); 108 | const response = await createRequestContext.get(url + `booking/summary?roomid=${bookingId}`); 109 | 110 | expect(response.status()).toBe(200); 111 | const body = await response.json(); 112 | return body; 113 | } 114 | 115 | /** 116 | * 117 | * @param bookingId number for the booking you want to see the details of 118 | * @returns the body of the booking/${bookingId} endpoint 119 | */ 120 | export async function getBookingById(bookingId: number) { 121 | const headers = await createHeaders(); 122 | 123 | const createRequestContext = await request.newContext(); 124 | const response = await createRequestContext.get(url + `booking/${bookingId}`, { 125 | headers: headers, 126 | }); 127 | 128 | expect(response.status()).toBe(200); 129 | const body = await response.json(); 130 | return body; 131 | } 132 | 133 | /** 134 | * 135 | * @param roomId 136 | * @returns the most future checkout date for a room 137 | * @example 138 | * 139 | * let futureCheckinDate = await futureOpenCheckinDate(roomId); // "2023-03-31T00:00:00.000Z" 140 | * let checkInString = futureCheckinDate.toISOString().split("T")[0]; // "2023-03-31" 141 | * let checkOutString = stringDateByDays(futureCheckinDate, 2); // "2023-04-02" 142 | */ 143 | export async function futureOpenCheckinDate(roomId: number) { 144 | const currentBookings = await getBookings(roomId); 145 | 146 | checkOutArray = []; 147 | 148 | // Iterate through current bookings and get checkout dates 149 | for (let i = 0; i < (await currentBookings.bookings.length); i++) { 150 | const today = new Date(); 151 | const checkOut = new Date(currentBookings.bookings[i].bookingdates.checkout); 152 | 153 | if (today < checkOut) { 154 | // pushing the checkout date into an array 155 | checkOutArray.push(checkOut); 156 | } 157 | } 158 | 159 | // Find the most future checkout date and return it if no future dates exist return today 160 | const mostFutureDate = 161 | checkOutArray 162 | .sort(function (a, b) { 163 | return a - b; 164 | }) 165 | .pop() || new Date(); 166 | 167 | // console.log("Last Checkout Date: " + mostFutureDate); 168 | return mostFutureDate; 169 | } 170 | -------------------------------------------------------------------------------- /lib/datafactory/message.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import Env from "@helpers/env"; 3 | import { request, expect } from "@playwright/test"; 4 | 5 | const url = Env.URL || "https://automationintesting.online/"; 6 | 7 | export const message1Summary = { 8 | id: 1, 9 | name: "James Dean", 10 | subject: "Booking enquiry", 11 | read: false, 12 | }; 13 | 14 | export const message1 = { 15 | messageid: 1, 16 | name: "James Dean", 17 | email: "james@email.com", 18 | phone: "01402 619211", 19 | subject: "Booking enquiry", 20 | description: "I would like to book a room at your place", 21 | }; 22 | 23 | export const postMessage = { 24 | name: "string", 25 | email: "test@test.com", 26 | phone: "stringstringstring", 27 | subject: "string", 28 | description: "stringstringstringst", 29 | }; 30 | 31 | export async function newMessageBody() { 32 | const message = { 33 | name: faker.person.firstName(), 34 | email: faker.internet.email(), 35 | phone: faker.phone.number(), 36 | subject: faker.company.buzzPhrase(), 37 | description: faker.company.catchPhrase(), 38 | }; 39 | return message; 40 | } 41 | 42 | export async function createMessage() { 43 | const message = await newMessageBody(); 44 | 45 | const contextRequest = await request.newContext(); 46 | const response = await contextRequest.post(url + "message/", { 47 | data: message, 48 | }); 49 | 50 | expect(response.status()).toBe(201); 51 | const body = await response.json(); 52 | return body; 53 | } 54 | -------------------------------------------------------------------------------- /lib/datafactory/room.ts: -------------------------------------------------------------------------------- 1 | import { expect, request } from "@playwright/test"; 2 | import { faker } from "@faker-js/faker"; 3 | import { createHeaders } from "../helpers/createHeaders"; 4 | import { randomRoomFeaturesCount } from "@helpers/roomFeatures"; 5 | import Env from "@helpers/env"; 6 | 7 | const url = Env.URL || "https://automationintesting.online/"; 8 | 9 | export async function createRandomRoomBody(roomName?: string, roomPrice?: number) { 10 | const roomType = ["Single", "Double", "Twin"]; 11 | const features = randomRoomFeaturesCount(6); 12 | 13 | const roomBody = { 14 | roomName: roomName || faker.string.numeric(3), 15 | type: roomType[Math.floor(Math.random() * roomType.length)], // returns a random value from the array 16 | accessible: Math.random() < 0.5, //returns true or false 17 | image: "/images/room2.jpg", 18 | // image: faker.image.urlLoremFlickr({ 19 | // category: "cat", 20 | // width: 500, 21 | // height: 500, 22 | // }), 23 | description: faker.hacker.phrase(), 24 | features: features.sort(() => 0.5 - Math.random()).slice(0, 3), // returns 3 random values from the array 25 | roomPrice: roomPrice || faker.string.numeric(3), 26 | }; 27 | 28 | return roomBody; 29 | } 30 | 31 | /** 32 | * This function will create a room with provided name and a price 33 | * 34 | * @param roomName: string for the room to create 35 | * @param roomPrice: number for the price of the room 36 | * @returns the body of the room just created with a unique roomid in the response 37 | * 38 | * @example 39 | * let room = await createRoom("My Room", 100); 40 | * let roomId = room.roomid; 41 | */ 42 | export async function createRoom(roomName?: string, roomPrice?: number) { 43 | const headers = await createHeaders(); 44 | 45 | const roomBody = await createRandomRoomBody(roomName, roomPrice); 46 | 47 | const createRequestContext = await request.newContext(); 48 | const response = await createRequestContext.post(url + "room/", { 49 | headers: headers, 50 | data: roomBody, 51 | }); 52 | 53 | expect(response.status()).toBe(201); 54 | const body = await response.json(); 55 | 56 | return body; 57 | } 58 | 59 | export const defaultRoom = { 60 | roomid: 1, 61 | roomName: "101", 62 | type: "single", 63 | accessible: true, 64 | image: "/images/room2.jpg", 65 | description: 66 | "Aenean porttitor mauris sit amet lacinia molestie. In posuere accumsan aliquet. Maecenas sit amet nisl massa. Interdum et malesuada fames ac ante.", 67 | features: ["TV", "WiFi", "Safe"], 68 | roomPrice: 100, 69 | }; 70 | -------------------------------------------------------------------------------- /lib/fixtures/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { mergeExpects } from "@playwright/test"; 2 | import { expect as toBeOneOfValuesExpect } from "@fixtures/toBeOneOfValues"; 3 | import { expect as toBeValidDate } from "@fixtures/toBeValidDate"; 4 | import { expect as typesExpects } from "@fixtures/typesExpects"; 5 | 6 | export { test } from "@playwright/test"; 7 | 8 | export const expect = mergeExpects(toBeOneOfValuesExpect, toBeValidDate, typesExpects); 9 | -------------------------------------------------------------------------------- /lib/fixtures/toBeOneOfValues.ts: -------------------------------------------------------------------------------- 1 | import { expect as baseExpect } from "@playwright/test"; 2 | 3 | export { test } from "@playwright/test"; 4 | 5 | export const expect = baseExpect.extend({ 6 | toBeOneOfValues(received: any, array: any[]) { 7 | const pass = array.includes(received); 8 | if (pass) { 9 | return { 10 | message: () => "passed", 11 | pass: true, 12 | }; 13 | } else { 14 | return { 15 | message: () => `toBeOneOfValues() assertion failed.\nYou expected [${array}] to include '${received}'\n`, 16 | pass: false, 17 | }; 18 | } 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /lib/fixtures/toBeValidDate.ts: -------------------------------------------------------------------------------- 1 | import { expect as baseExpect } from "@playwright/test"; 2 | 3 | export { test } from "@playwright/test"; 4 | 5 | export const expect = baseExpect.extend({ 6 | toBeValidDate(received: any) { 7 | const pass = Date.parse(received) && typeof received === "string" ? true : false; 8 | if (pass) { 9 | return { 10 | message: () => "passed", 11 | pass: true, 12 | }; 13 | } else { 14 | return { 15 | message: () => `toBeValidDate() assertion failed.\nYou expected '${received}' to be a valid date.\n`, 16 | pass: false, 17 | }; 18 | } 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /lib/fixtures/typesExpects.ts: -------------------------------------------------------------------------------- 1 | import { expect as baseExpect } from "@playwright/test"; 2 | 3 | export { test } from "@playwright/test"; 4 | 5 | export const expect = baseExpect.extend({ 6 | toBeOneOfTypes(received: any, array: string[]) { 7 | const pass = array.includes(typeof received) || (array.includes(null) && received == null); 8 | 9 | if (pass) { 10 | return { 11 | message: () => "passed", 12 | pass: true, 13 | }; 14 | } else { 15 | return { 16 | message: () => 17 | `toBeOneOfTypes() assertion failed.\nYou expected '${ 18 | received == null ? "null" : typeof received 19 | }' type to be one of [${array}] types\n${ 20 | array.includes(null) 21 | ? `WARNING: [${array}] array contains 'null' type which is not printed in the error\n` 22 | : null 23 | }`, 24 | pass: false, 25 | }; 26 | } 27 | }, 28 | 29 | toBeNumber(received: any) { 30 | const pass = typeof received == "number"; 31 | if (pass) { 32 | return { 33 | message: () => "passed", 34 | pass: true, 35 | }; 36 | } else { 37 | return { 38 | message: () => 39 | `toBeNumber() assertion failed.\nYou expected '${received}' to be a number but it's a ${typeof received}\n`, 40 | pass: false, 41 | }; 42 | } 43 | }, 44 | 45 | toBeString(received: any) { 46 | const pass = typeof received == "string"; 47 | if (pass) { 48 | return { 49 | message: () => "passed", 50 | pass: true, 51 | }; 52 | } else { 53 | return { 54 | message: () => 55 | `toBeString() assertion failed.\nYou expected '${received}' to be a string but it's a ${typeof received}\n`, 56 | pass: false, 57 | }; 58 | } 59 | }, 60 | 61 | toBeBoolean(received: any) { 62 | const pass = typeof received == "boolean"; 63 | if (pass) { 64 | return { 65 | message: () => "passed", 66 | pass: true, 67 | }; 68 | } else { 69 | return { 70 | message: () => 71 | `toBeBoolean() assertion failed.\nYou expected '${received}' to be a boolean but it's a ${typeof received}\n`, 72 | pass: false, 73 | }; 74 | } 75 | }, 76 | 77 | toBeObject(received: any) { 78 | const pass = typeof received == "object"; 79 | if (pass) { 80 | return { 81 | message: () => "passed", 82 | pass: true, 83 | }; 84 | } else { 85 | return { 86 | message: () => 87 | `toBeObject() assertion failed.\nYou expected '${received}' to be an object but it's a ${typeof received}\n`, 88 | pass: false, 89 | }; 90 | } 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /lib/helpers/arrayFunctions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Call valueExists function with an array of objects, a key, and value. 3 | The function will return true or false if it found the value specified in the array 4 | 5 | body.items is an array of objects from the customers table, 6 | last_name is a key in the array of objects, 7 | "LaRusso" is the value we are trying to match 8 | 9 | valueExists(body.items, "last_name", "LaRusso") //returns true or false 10 | */ 11 | 12 | export function valueExistsInArray(array: [], key: string, value: string) { 13 | return array.some(function (el) { 14 | return el[key] === value; 15 | }); 16 | } 17 | 18 | export function removeItemsFromArray(arrayToFilter: string[], itemsToRemove: string[]) { 19 | itemsToRemove.forEach((itemToRemove) => { 20 | arrayToFilter = arrayToFilter.filter((arrayItem) => arrayItem !== itemToRemove); 21 | }); 22 | 23 | return arrayToFilter; 24 | } 25 | -------------------------------------------------------------------------------- /lib/helpers/branding.ts: -------------------------------------------------------------------------------- 1 | export const defaultBranding = { 2 | name: "Shady Meadows B&B", 3 | map: { 4 | latitude: 52.6351204, 5 | longitude: 1.2733774, 6 | }, 7 | logoUrl: "https://automationintesting.online/images/rbp-logo.jpg", 8 | description: 9 | "Welcome to Shady Meadows, a delightful Bed & Breakfast nestled in the hills on Newingtonfordburyshire. A place so beautiful you will never want to leave. All our rooms have comfortable beds and we provide breakfast from the locally sourced supermarket. It is a delightful place.", 10 | contact: { 11 | name: "Shady Meadows B&B", 12 | address: "The Old Farmhouse, Shady Street, Newfordburyshire, NE1 410S", 13 | phone: "012345678901", 14 | email: "fake@fakeemail.com", 15 | }, 16 | }; 17 | 18 | export const defaultBrandingShortLogo = { 19 | name: "Shady Meadows B&B", 20 | map: { 21 | latitude: 52.6351204, 22 | longitude: 1.2733774, 23 | }, 24 | logoUrl: "/images/rbp-logo.jpg", 25 | description: 26 | "Welcome to Shady Meadows, a delightful Bed & Breakfast nestled in the hills on Newingtonfordburyshire. A place so beautiful you will never want to leave. All our rooms have comfortable beds and we provide breakfast from the locally sourced supermarket. It is a delightful place.", 27 | contact: { 28 | name: "Shady Meadows B&B", 29 | address: "The Old Farmhouse, Shady Street, Newfordburyshire, NE1 410S", 30 | phone: "012345678901", 31 | email: "fake@fakeemail.com", 32 | }, 33 | }; 34 | 35 | export const updatedBranding = { 36 | name: "Test Name", 37 | map: { 38 | latitude: 41.8781, 39 | longitude: 87.6298, 40 | }, 41 | logoUrl: "https://media.tenor.com/KaCUHzQxVWcAAAAC/house.gif", 42 | description: "description", 43 | contact: { 44 | name: "Testy McTester", 45 | address: "100 Testing Way", 46 | phone: "5555555555", 47 | email: "testy@testymtesterface.com", 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /lib/helpers/capitalizeString.ts: -------------------------------------------------------------------------------- 1 | // capitalizes the first letter in a string 2 | 3 | import { fail } from "assert"; 4 | 5 | export function capitalizeString(stringToCapitalize: string) { 6 | if (stringToCapitalize === "") fail("The string you passed is empty"); 7 | 8 | return stringToCapitalize[0].toUpperCase() + stringToCapitalize.slice(1); 9 | } 10 | -------------------------------------------------------------------------------- /lib/helpers/coverage.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@playwright/test"; 2 | import * as fs from "fs"; 3 | import { execSync } from "child_process"; 4 | import "dotenv/config"; 5 | import Env from "@helpers/env"; 6 | 7 | const baseURL = Env.URL; 8 | 9 | /** 10 | * 11 | * @param endpoint url path for pulling the OpenAPI spec 12 | * @example getEndpointCoverage("auth"); console logs coverage for auth endpoints 13 | */ 14 | export async function getEndpointCoverage(endpoint: string) { 15 | console.log(`=== Coverage for ${endpoint} Endpoints ===`); 16 | const response = await fetchOpenApi(endpoint); 17 | const coverageArray = getEndpoints(response); 18 | getCoverage(coverageArray); 19 | } 20 | 21 | /** 22 | * 23 | * @param resource 24 | * @returns JSON object of the OpenAPI spec 25 | * 26 | * @example await fetchOpenApi("messages"); returns JSON object of the OpenAPI spec 27 | * 28 | * There is also a ${resource}_spec3.json file created in the root of the project 29 | * These files are used to get the endpoints and calculate coverage 30 | * 31 | */ 32 | export async function fetchOpenApi(resource: string) { 33 | const requestContext = await request.newContext(); 34 | const response = await requestContext.get(`${baseURL}${resource}/v3/api-docs/${resource}-api`, { timeout: 5000 }); 35 | 36 | const body = await response.json(); 37 | writeFile(`./${resource}_spec3.json`, JSON.stringify(body, null, 2)); 38 | return body; 39 | } 40 | 41 | /** 42 | * 43 | * @param json JSON object of the OpenAPI spec 44 | * @returns Array of endpoints with format "VERB PATH" 45 | * @example getEndpoints(authJson); returns ["POST /auth/login", "POST /auth/logout", ...] 46 | * 47 | * This function is used to get the endpoints from the OpenAPI spec 48 | */ 49 | export function getEndpoints(json) { 50 | const spec3 = json; 51 | 52 | const methods = spec3.paths; 53 | const urlPath = spec3.servers[0].url.slice(0, -1); 54 | 55 | const finalArray: string[] = []; 56 | for (const property in methods) { 57 | const verbs = Object.keys(methods[property]); 58 | for (const verb of verbs) { 59 | const finalVerb = verb.toUpperCase(); 60 | const finalPath = urlPath + property; 61 | finalArray.push(finalVerb + " " + finalPath); 62 | } 63 | } 64 | return finalArray; 65 | } 66 | 67 | //Greps local files getting a list of files with specified coverage tag and calculates coverage 68 | export function getCoverage(coverageArray) { 69 | const totalEndPoints = coverageArray.length; 70 | let coveredEndPoints = 0; 71 | const nonCoveredEndpoints: string[] = []; 72 | 73 | //Iterates through the coverageArray to grep each file in the test directory looking for matches 74 | for (const value in coverageArray) { 75 | const output = execSync(`grep -rl tests -e 'COVERAGE_TAG: ${coverageArray[value]}$' | cat`, { 76 | encoding: "utf-8", 77 | }); 78 | // console.log(value); 79 | // console.log(coverageArray[value]); 80 | // console.log(output); 81 | if (output != "") { 82 | coveredEndPoints += 1; 83 | } else { 84 | console.log(`Endpoint with no coverage: ${coverageArray[value]}`); 85 | nonCoveredEndpoints.push(coverageArray[value]); 86 | } 87 | } 88 | 89 | console.log("Total Endpoints: " + totalEndPoints); 90 | console.log("Covered Endpoints: " + coveredEndPoints); 91 | // writeFile( 92 | // "./lib/non_covered_endpoints.txt", 93 | // JSON.stringify(nonCoveredEndpoints, null, "\t") 94 | // ); 95 | calculateCoverage(coveredEndPoints, totalEndPoints); 96 | } 97 | 98 | function calculateCoverage(coveredEndpoints: number, totalEndpoints: number) { 99 | const percentCovered = ((coveredEndpoints / totalEndpoints) * 100).toFixed(2); 100 | console.log("Coverage: " + percentCovered + "%"); 101 | process.env.COVERED_ENDPOINTS = coveredEndpoints.toString(); 102 | process.env.TOTAL_ENDPOINTS = totalEndpoints.toString(); 103 | process.env.PERCENT_COVERED = percentCovered.toString(); 104 | } 105 | 106 | // eslint-disable-next-line 107 | function writeFile(location: string, data: string) { 108 | try { 109 | fs.writeFileSync(location, data); 110 | // console.log("File written successfully"); 111 | // console.log("The written file has" + " the following contents:"); 112 | // console.log("" + fs.readFileSync(location)); 113 | } catch (err) { 114 | console.error(err); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/helpers/createAssertions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/prefer-for-of */ 2 | 3 | /* 4 | this function logs in console ready to use expects 5 | example: passing the following object (body) to the function 6 | { 7 | "one": 1, 8 | "two": "2", 9 | "three": { 10 | "four": ["4", "cuatro"], 11 | "five": [ 12 | { 13 | "six": [] 14 | }, 15 | { 16 | "seven": null 17 | } 18 | ] 19 | } 20 | } 21 | 22 | would generate the following ready to use assertions: 23 | 24 | expect(body.one).toBe(1); 25 | expect(body.two).toBe("2"); 26 | expect(body.three.four).toEqual(["4","cuatro"]); 27 | expect(body.three.five[0].six).toEqual([]); 28 | expect(body.three.five[1].seven).toBe(null); 29 | */ 30 | export async function createAssertions(object: object, paramName = "body"): Promise { 31 | for (const key in object) { 32 | const value = object[key]; 33 | 34 | if (typeof value === "string") { 35 | console.log(`expect(${paramName}.${key}).toBe("${value}");`); 36 | } else if (value === null) { 37 | console.log(`expect(${paramName}.${key}).toBeNull();`); 38 | } else if (typeof value === "number") { 39 | console.log(`expect(${paramName}.${key}).toBe(${value});`); 40 | } else if (typeof value === "object") { 41 | if (Array.isArray(value)) { 42 | if (value.length === 0) { 43 | console.log(`expect(${paramName}.${key}).toEqual([]);`); 44 | } else if (typeof value[0] === "object") { 45 | createAssertions(value, `${paramName}.${key}`); 46 | } else { 47 | const newArray = value.map((item: string | number | null) => 48 | typeof item === "string" ? `"${item}"` : (item as number) 49 | ); 50 | console.log(`expect(${paramName}.${key}).toEqual([${newArray}]);`); 51 | } 52 | } else if (Object.keys(value).length === 0) { 53 | console.log(`expect(${paramName}.${key}).toEqual({});`); 54 | } else if (parseInt(key) >= 0) { 55 | createAssertions(value, `${paramName}[${key}]`); 56 | } else { 57 | createAssertions(value, `${paramName}.${key}`); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/helpers/createHeaders.ts: -------------------------------------------------------------------------------- 1 | import { createCookies } from "../datafactory/auth"; 2 | import Env from "@helpers/env"; 3 | 4 | const username = Env.ADMIN_NAME; 5 | const password = Env.ADMIN_PASSWORD; 6 | 7 | /** 8 | * 9 | * @param token a valid token to be used in the request if one is not provided cookies will be created from default username and password 10 | * @returns a header object with the token set as a cookie 11 | * 12 | * @example 13 | * import { createHeaders } from "../lib/helpers/createHeaders"; 14 | * 15 | * const headers = await createHeaders(token); 16 | * const response = await request.delete(`booking/${bookingId}`, { 17 | headers: headers, 18 | }); 19 | * 20 | */ 21 | export async function createHeaders(token?: string): Promise { 22 | let requestHeaders: RequestHeaders; 23 | 24 | if (token) { 25 | requestHeaders = { 26 | cookie: `token=${token}`, 27 | }; 28 | } else { 29 | // Authenticate and get cookies 30 | const cookies = await createCookies(username, password); 31 | requestHeaders = { 32 | cookie: cookies, 33 | }; 34 | } 35 | 36 | return requestHeaders; 37 | } 38 | 39 | /** 40 | * 41 | * @returns a header object with an invalid cookie used to test negative scenarios 42 | * 43 | * @example 44 | * import { createInvalidHeaders } from "../lib/helpers/createHeaders"; 45 | * 46 | * const invalidHeader = await createInvalidHeaders(); 47 | * const response = await request.delete(`booking/${bookingId}`, { 48 | headers: invalidHeader, 49 | }); 50 | * 51 | */ 52 | export async function createInvalidHeaders() { 53 | const requestHeaders = { 54 | cookie: "cookie=invalid", 55 | }; 56 | 57 | return requestHeaders; 58 | } 59 | 60 | interface RequestHeaders { 61 | cookie: string; 62 | } 63 | -------------------------------------------------------------------------------- /lib/helpers/date.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | 3 | /** 4 | * Function takes a Date and a number of days to add/subtract from today's date 5 | * if you need to subtract days pass a negative number 6 | * example: -1 wil return yesterday's date while passing 1 will return tomorrow 7 | * 8 | * @example 9 | * import { stringDateByDays } from "../helpers/date"; 10 | * 11 | * let checkOutString = stringDateByDays(today, 5); 12 | * console.log(checkOutString) // 2023-03-24 13 | * 14 | * let checkOutString = stringDateByDays(); 15 | * console.log(checkOutString) // 2023-03-19 16 | */ 17 | export function stringDateByDays(date?: Date, days = 0) { 18 | const today = date || new Date(); 19 | if (days === 0) { 20 | return today.toISOString().split("T")[0]; 21 | } else { 22 | const newDate = new Date(today.setDate(today.getDate() + days)); 23 | return newDate.toISOString().split("T")[0]; 24 | } 25 | } 26 | 27 | /** 28 | * Function takes a date as a string and validates that it can be parsed by Date.parse() 29 | * It returns a true or false, great for asserting of the data is properly formatted. 30 | */ 31 | function isValidDate(date: string) { 32 | if (Date.parse(date)) { 33 | return true; 34 | } else { 35 | return false; 36 | } 37 | } 38 | 39 | // converts date to epoch number so it's easier to compare them 40 | export function convertToEpoch(date: string): number | bigint { 41 | if (isValidDate(date)) { 42 | return Date.parse(date); 43 | } 44 | } 45 | 46 | /* 47 | it's hard to predict what date some parameters are and if they are even a date 48 | instead we get them in an array and iterate to check 49 | if they are either a string or null 50 | */ 51 | export function checkObjectDateValues(object) { 52 | const dates = Object.keys(object).filter((key) => key.includes("_at")); 53 | dates.forEach((date) => { 54 | object[date] ? expect(isValidDate(object[date])).toBe(true) : expect(object[date]).toBe(null); 55 | }); 56 | } 57 | 58 | export function todayDayAsNumberUTC() { 59 | return new Date().getDate(); 60 | } 61 | 62 | export function todayDayAsNumberTimezone(timezone = "America/Chicago") { 63 | const date = new Date(); 64 | 65 | return Number(date.toLocaleString("en-US", { timeZone: timezone }).split("/")[1]); 66 | } 67 | 68 | export function todayMonthAsNumber() { 69 | return new Date().getMonth(); 70 | } 71 | 72 | export function todayYearAsNumber() { 73 | return new Date().getFullYear(); 74 | } 75 | -------------------------------------------------------------------------------- /lib/helpers/env.ts: -------------------------------------------------------------------------------- 1 | export default class Env { 2 | public static readonly URL = process.env.URL; 3 | public static readonly ADMIN_NAME = process.env.ADMIN_NAME; 4 | public static readonly ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; 5 | public static readonly SECRET_API_KEY = process.env.SECRET_API_KEY; 6 | } 7 | -------------------------------------------------------------------------------- /lib/helpers/roomFeatures.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | 3 | const roomFeatures = [ 4 | "TV", 5 | "WiFi", 6 | "Safe", 7 | "Mini Bar", 8 | "Tea/Coffee", 9 | "Balcony", 10 | "Bath", 11 | "Shower", 12 | "Sea View", 13 | "Mountain View", 14 | "City View", 15 | "River View", 16 | "Garden View", 17 | "Pool View", 18 | "Patio", 19 | "Terrace", 20 | "Air Conditioning", 21 | "Heating", 22 | "Kitchen", 23 | "Dining Area", 24 | "Sofa", 25 | "Fireplace", 26 | "Private Entrance", 27 | "Soundproofing", 28 | "Wardrobe", 29 | "Clothes Rack", 30 | "Ironing Facilities", 31 | "Desk", 32 | "Seating Area", 33 | "Sofa Bed", 34 | ]; 35 | 36 | export function allRoomFeatures() { 37 | return roomFeatures; 38 | } 39 | 40 | export function randomRoomFeatures() { 41 | return roomFeatures[faker.number.int({ min: 0, max: roomFeatures.length - 1 })]; 42 | } 43 | 44 | export function randomRoomFeaturesCount(count: number) { 45 | const features = []; 46 | 47 | for (let i = 0; i < count; i++) { 48 | features.push(randomRoomFeatures()); 49 | } 50 | // This will remove all duplicates from the array 51 | return Array.from(new Set(features)); 52 | } 53 | -------------------------------------------------------------------------------- /lib/helpers/schemaData/Auth.ts: -------------------------------------------------------------------------------- 1 | // updated on 2023-09-23 2 | 3 | export const authSchemaExpectedResponseParamsCount = { 4 | Token: 1, 5 | Auth: 2, 6 | }; 7 | -------------------------------------------------------------------------------- /lib/helpers/schemaData/Booking.ts: -------------------------------------------------------------------------------- 1 | // updated on 2023-09-23 2 | 3 | export const bookingSchemaExpectedResponseParamsCount = { 4 | Error: 4, 5 | Booking: 8, 6 | BookingDates: 2, 7 | CreatedBooking: 2, 8 | }; 9 | -------------------------------------------------------------------------------- /lib/helpers/schemaData/Branding.ts: -------------------------------------------------------------------------------- 1 | // updated on 2023-09-23 2 | 3 | export const brandingSchemaExpectedResponseParamsCount = { 4 | Error: 4, 5 | Branding: 5, 6 | Contact: 4, 7 | Map: 2, 8 | }; 9 | -------------------------------------------------------------------------------- /lib/helpers/schemaData/Message.ts: -------------------------------------------------------------------------------- 1 | // updated on 2023-09-23 2 | 3 | export const messageSchemaExpectedResponseParamsCount = { 4 | Error: 4, 5 | Message: 6, 6 | Count: 1, 7 | MessageSummary: 4, 8 | Messages: 1, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/helpers/schemaData/Report.ts: -------------------------------------------------------------------------------- 1 | // updated on 2023-09-23 2 | 3 | export const reportSchemaExpectedResponseParamsCount = { 4 | Entry: 3, 5 | Report: 1, 6 | }; 7 | -------------------------------------------------------------------------------- /lib/helpers/schemaData/Room.ts: -------------------------------------------------------------------------------- 1 | // updated on 2023-09-23 2 | 3 | export const roomSchemaExpectedResponseParamsCount = { 4 | Error: 4, 5 | Room: 8, 6 | Rooms: 1, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/helpers/schemaHelperFunctions.ts: -------------------------------------------------------------------------------- 1 | import { createSchema } from "genson-js"; 2 | import * as fs from "fs/promises"; 3 | 4 | export async function createJsonSchema(name: string, path: string, json: object) { 5 | const filePath = `./.api/${path}`; 6 | 7 | try { 8 | await fs.mkdir(filePath, { recursive: true }); 9 | 10 | const schema = createSchema(json); 11 | const schemaString = JSON.stringify(schema, null, 2); 12 | const schemaFilePath = `.api/${path}/${name}_schema.json`; 13 | 14 | await writeJsonFile(schemaFilePath, schemaString); 15 | 16 | console.log("JSON Schema created and saved."); 17 | } catch (err) { 18 | console.error(err); 19 | } 20 | } 21 | 22 | export async function writeJsonFile(location: string, data: string) { 23 | try { 24 | await fs.writeFile(location, data); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/helpers/tests/createAssertions.test.ts: -------------------------------------------------------------------------------- 1 | import { createAssertions } from "@helpers/createAssertions"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-empty-function 4 | const log = jest.spyOn(console, "log").mockImplementation(() => {}); 5 | 6 | describe("createAssertions", () => { 7 | test("createAssertions logs proper assertions to console", async () => { 8 | const input = { 9 | one: 1, 10 | two: "2", 11 | three: { 12 | four: ["4", "cuatro"], 13 | five: [ 14 | { 15 | six: [], 16 | }, 17 | { 18 | seven: null, 19 | }, 20 | ], 21 | }, 22 | }; 23 | const expectedLogConsoleCalls = [ 24 | ["expect(body.one).toBe(1);"], 25 | ['expect(body.two).toBe("2");'], 26 | ['expect(body.three.four).toEqual(["4","cuatro"]);'], 27 | ["expect(body.three.five[0].six).toEqual([]);"], 28 | ["expect(body.three.five[1].seven).toBeNull();"], 29 | ]; 30 | 31 | await createAssertions(input); 32 | 33 | expect(log.mock.calls).toEqual(expectedLogConsoleCalls); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/helpers/tests/createHeaders.test.ts: -------------------------------------------------------------------------------- 1 | import { createHeaders, createInvalidHeaders } from "@helpers/createHeaders"; 2 | import { createCookies } from "@datafactory/auth"; 3 | import Env from "@helpers/env"; 4 | 5 | jest.mock("@helpers/env", () => ({ 6 | __esModule: true, 7 | default: { 8 | URL: "MockedUrl", 9 | ADMIN_NAME: "MockedAdminName", 10 | ADMIN_PASSWORD: "MockedAdminPassword", 11 | SECRET_API_KEY: "MockedSecretApiKey", 12 | }, 13 | namedExport: jest.fn(), 14 | })); 15 | jest.mock("@datafactory/auth"); 16 | 17 | describe("createHeaders", () => { 18 | test("createHeaders return header with given token", async () => { 19 | const input = "mockedToken"; 20 | const expected = { 21 | cookie: `token=${input}`, 22 | }; 23 | 24 | const actual = await createHeaders(input); 25 | 26 | expect(actual).toEqual(expected); 27 | }); 28 | test("createHeaders call createCookies with credentials from Env when token is not given", async () => { 29 | const createCookiesMock = jest.mocked(createCookies); 30 | createCookiesMock.mockResolvedValue("token=mockedToken"); 31 | 32 | const expected = { 33 | cookie: `token=${"mockedToken"}`, 34 | }; 35 | 36 | const actual = await createHeaders(); 37 | 38 | expect(createCookies).toHaveBeenCalledWith(Env.ADMIN_NAME, Env.ADMIN_PASSWORD); 39 | expect(actual).toEqual(expected); 40 | }); 41 | 42 | describe("createInvalidHeaders", () => { 43 | test("should return header with invalid cookie", async () => { 44 | const expected = { 45 | cookie: "cookie=invalid", 46 | }; 47 | 48 | const actual = await createInvalidHeaders(); 49 | 50 | expect(actual).toEqual(expected); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/helpers/tests/schemaHelperFunctions.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { createJsonSchema, writeJsonFile } from "@helpers/schemaHelperFunctions"; 2 | import * as fs from "fs/promises"; 3 | 4 | describe("schemaHelperFunctions Integration Tests", () => { 5 | describe("writeJsonFile", () => { 6 | const inputFile = "lib/helpers/tests/fake.json"; 7 | 8 | afterAll(() => { 9 | fs.unlink(inputFile); 10 | }); 11 | 12 | test("writeJsonFile should write data properly to the given path", async () => { 13 | const mockedData = JSON.stringify({ some: "MockedField" }); 14 | 15 | await writeJsonFile(inputFile, mockedData); 16 | 17 | const data = await fs.readFile(inputFile, { encoding: "utf8" }); 18 | expect(data).toBe(mockedData); 19 | }); 20 | }); 21 | describe("createJsonSchema", () => { 22 | const endpointDir = "mocked_endpoint"; 23 | const endpointFullPath = `.api/${endpointDir}`; 24 | const schemaName = "get_mocked"; 25 | 26 | beforeAll(() => { 27 | fs.mkdir(endpointFullPath); 28 | }); 29 | 30 | afterAll(() => { 31 | fs.rm(endpointFullPath, { recursive: true, force: true }); 32 | }); 33 | 34 | test("should create and write a new schema file to the endpoints directory", async () => { 35 | const endpointSchemaExpectedPath = `${endpointFullPath}/${schemaName}_schema.json`; 36 | const mockedData = { some: "MockedField" }; 37 | const expected = { 38 | type: "object", 39 | properties: { 40 | some: { 41 | type: "string", 42 | }, 43 | }, 44 | required: ["some"], 45 | }; 46 | 47 | await createJsonSchema(schemaName, endpointDir, mockedData); 48 | 49 | const data = await fs.readFile(endpointSchemaExpectedPath, { encoding: "utf8" }); 50 | 51 | expect(JSON.parse(data)).toEqual(expected); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /lib/helpers/tests/schemaHelperFunctions.test.ts: -------------------------------------------------------------------------------- 1 | import { createJsonSchema, writeJsonFile } from "@helpers/schemaHelperFunctions"; 2 | import * as fs from "fs/promises"; 3 | import { Schema, createSchema } from "genson-js"; 4 | 5 | jest.mock("fs/promises"); 6 | jest.mock("genson-js"); 7 | 8 | describe("schemaHelperFunctions", () => { 9 | beforeEach(() => { 10 | jest.clearAllMocks(); 11 | }); 12 | describe("writeJsonFile", () => { 13 | test("should call fs.writeFile with given filePath and Data", async () => { 14 | const inputFile = "mocked/input/path.json"; 15 | const mockedData = JSON.stringify({ someMocked: "MockedField" }); 16 | 17 | await writeJsonFile(inputFile, mockedData); 18 | 19 | expect(fs.writeFile).toBeCalledWith(inputFile, mockedData); 20 | }); 21 | }); 22 | describe("createJsonSchema", () => { 23 | const createSchemaMock = jest.mocked(createSchema); 24 | test("should create schema using createSchema from genson-js and write file using fs/promises", async () => { 25 | const endpointPath = "mocked_endpoint"; 26 | const schemaFile = "get_mocked"; 27 | const expectedFullPath = ".api/mocked_endpoint/get_mocked_schema.json"; 28 | const mockedData = { someMocked: "MockedField" }; 29 | const mockedSchema = { 30 | type: "object", 31 | properties: { 32 | someMocked: { 33 | type: "string", 34 | }, 35 | }, 36 | required: ["someMocked"], 37 | } as Schema; 38 | createSchemaMock.mockReturnValueOnce(mockedSchema); 39 | const expectedSchemaJsonContent = JSON.stringify(mockedSchema, null, 2); 40 | 41 | await createJsonSchema(schemaFile, endpointPath, mockedData); 42 | 43 | expect(createSchema).toBeCalledWith(mockedData); 44 | expect(fs.writeFile).toBeCalledWith(expectedFullPath, expectedSchemaJsonContent); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /lib/helpers/validateAgainstSchema.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { expect, test } from "@playwright/test"; 3 | import { removeItemsFromArray } from "@helpers/arrayFunctions"; 4 | import { capitalizeString } from "@helpers/capitalizeString"; 5 | import { fail } from "assert"; 6 | import { addWarning } from "@helpers/warnings"; 7 | import { authSchemaExpectedResponseParamsCount } from "@helpers/schemaData/Auth"; 8 | import { bookingSchemaExpectedResponseParamsCount } from "@helpers/schemaData/Booking"; 9 | import { brandingSchemaExpectedResponseParamsCount } from "@helpers/schemaData/Branding"; 10 | import { messageSchemaExpectedResponseParamsCount } from "@helpers/schemaData/Message"; 11 | import { reportSchemaExpectedResponseParamsCount } from "@helpers/schemaData/Report"; 12 | import { roomSchemaExpectedResponseParamsCount } from "@helpers/schemaData/Room"; 13 | import { stringDateByDays } from "@helpers/date"; 14 | 15 | /** 16 | * `Definition:` Validates an **object** against a specified **schema object** 17 | * @param object - The object to check schemaObject against. 18 | * @param schemaObject - The schema object name as documented in *_spec3.json. 19 | * @param docs - The type of docs (e.g. `public`, `internal` or `admin`). 20 | * @param notReturnedButInSchema - Any defined properties in schema but not returned by Falcon. 21 | * @param extraParamsReturned - Any undefined properties returned by Falcon; **_create a bug if there are any._** 22 | */ 23 | export async function validateAgainstSchema( 24 | object: object, 25 | schemaObject: string, 26 | docs: string, 27 | notReturnedButInSchema = [], 28 | extraParamsReturned = [] 29 | ) { 30 | // get keys from the object 31 | let responseObjectKeys = Object.keys(object); 32 | 33 | // get keys from the docs 34 | const schema = await schemaParameters(schemaObject, docs); 35 | let docsObjectKeys = Object.keys(schema); 36 | 37 | /* 38 | if used - workaround around a bug 39 | this should not be ok when we have more params in a response than in docs 40 | 41 | filter out extra params from the response params array if any 42 | */ 43 | if (extraParamsReturned.length > 0) { 44 | responseObjectKeys = removeItemsFromArray(responseObjectKeys, extraParamsReturned); 45 | } 46 | 47 | // filter out hidden params from the schema params array if any 48 | if (notReturnedButInSchema.length > 0) { 49 | docsObjectKeys = removeItemsFromArray(docsObjectKeys, notReturnedButInSchema); 50 | } 51 | 52 | // compare object keys (need to be sorted since order differs) 53 | expect(docsObjectKeys.sort()).toEqual(responseObjectKeys.sort()); 54 | 55 | // add a warning if schema object length has been changed based on doc types 56 | let recordedSchemaResponseParamsCount; 57 | if (docs === "auth") { 58 | recordedSchemaResponseParamsCount = authSchemaExpectedResponseParamsCount; 59 | } else if (docs === "booking") { 60 | recordedSchemaResponseParamsCount = bookingSchemaExpectedResponseParamsCount; 61 | } else if (docs === "branding") { 62 | recordedSchemaResponseParamsCount = brandingSchemaExpectedResponseParamsCount; 63 | } else if (docs === "message") { 64 | recordedSchemaResponseParamsCount = messageSchemaExpectedResponseParamsCount; 65 | } else if (docs === "report") { 66 | recordedSchemaResponseParamsCount = reportSchemaExpectedResponseParamsCount; 67 | } else if (docs === "room") { 68 | recordedSchemaResponseParamsCount = roomSchemaExpectedResponseParamsCount; 69 | } 70 | 71 | if (docsObjectKeys.length !== recordedSchemaResponseParamsCount[schemaObject] - notReturnedButInSchema.length) { 72 | addWarning( 73 | `'${schemaObject}' schema object in '${docs}' docs has been updated. Please, do the following: \n` + 74 | `- Check if the change is expected \n` + 75 | `- Update "${test.info().title}" test with appropriate assertions \n` + 76 | `- Re-run the test from terminal with 'GENERATE_SCHEMA_TRACKING_DATA=true', commit and push generated files \n\n` 77 | ); 78 | } 79 | } 80 | 81 | export async function schemaParameters(schema: string, docs: string) { 82 | try { 83 | const apiDocs = JSON.parse(fs.readFileSync(`./${docs}_spec3.json`).toString("utf-8")); 84 | 85 | return apiDocs.components.schemas[schema].properties; 86 | } catch (e) { 87 | fail(`The '${schema}' object you passed does not exist in '${docs}' documentation`); 88 | } 89 | } 90 | 91 | export async function updateDocsSchemasParamsCount() { 92 | const allDocs = ["auth", "booking", "branding", "message", "report", "room"]; 93 | 94 | allDocs.forEach((docs) => { 95 | const apiDocs = JSON.parse(fs.readFileSync(`./${docs}_spec3.json`).toString("utf-8")); 96 | const schemas = apiDocs.components.schemas; 97 | const schemaObjects = Object.keys(schemas); 98 | 99 | let data = ""; 100 | data += "// updated on " + stringDateByDays() + "\n\n"; 101 | data += `export const ${docs}SchemaExpectedResponseParamsCount = {\n`; 102 | schemaObjects.forEach((schema) => { 103 | data += ` ${schema}: ${Object.keys(schemas[schema].properties).length},\n`; 104 | }); 105 | data += "};\n"; 106 | 107 | try { 108 | fs.writeFileSync(`./lib/helpers/schemaData/${capitalizeString(docs)}.ts`, data); 109 | } catch (err) { 110 | console.error(err); 111 | } 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /lib/helpers/validateJsonSchema.ts: -------------------------------------------------------------------------------- 1 | import { createJsonSchema } from "@helpers/schemaHelperFunctions"; 2 | import { expect } from "@playwright/test"; 3 | import Ajv from "ajv"; 4 | 5 | /** 6 | * Validates an object against a JSON schema. 7 | * 8 | * @param {string} fileName - The first part of the name of the JSON schema file. The full name will be `${fileName}_schema.json`. 9 | * @param {string} filePath - The path to the directory containing the JSON schema file. 10 | * @param {object} body - The object to validate against the JSON schema. 11 | * @param {boolean} [createSchema=false] - Whether to create the JSON schema if it doesn't exist. 12 | * 13 | * @example 14 | * const body = await response.json(); 15 | * 16 | * // This will run the assertion against the existing schema file 17 | * await validateJsonSchema("POST_booking", "booking", body); 18 | * 19 | * // This will create or overwrite the schema file 20 | * await validateJsonSchema("POST_booking", "booking", body, true); 21 | */ 22 | export async function validateJsonSchema(fileName: string, filePath: string, body: object, createSchema = false) { 23 | const jsonName = fileName; 24 | const path = filePath; 25 | 26 | if (createSchema) { 27 | await createJsonSchema(jsonName, path, body); 28 | } 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-var-requires 31 | const existingSchema = require(`../../.api/${path}/${jsonName}_schema.json`); 32 | 33 | const ajv = new Ajv({ allErrors: false }); 34 | const validate = ajv.compile(existingSchema); 35 | const validRes = validate(body); 36 | 37 | if (!validRes) { 38 | console.log("SCHEMA ERRORS:", JSON.stringify(validate.errors), "\nRESPONSE BODY:", JSON.stringify(body)); 39 | } 40 | 41 | expect(validRes).toBe(true); 42 | } 43 | -------------------------------------------------------------------------------- /lib/helpers/warnings.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | export const warningsFile = "./warnings.log"; 4 | 5 | export async function addWarning(warning: string, warningsFileToUse = warningsFile) { 6 | fs.appendFile(warningsFileToUse, "WARNING: " + warning + "\n", (err) => { 7 | if (err) { 8 | console.error(err); 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-api-test-demo", 3 | "version": "1.0.0", 4 | "description": "This repository will serve as a place where I add API test Automation checks for articles written at ", 5 | "main": "index.js", 6 | "scripts": { 7 | "ut": "jest --verbose", 8 | "test": "npx playwright test --grep-invert=@unsatisfactory", 9 | "test:generate:schema": "GENERATE_SCHEMA_TRACKING_DATA=true npx playwright test --grep-invert=@unsatisfactory", 10 | "test:staging": "test_env=staging npx playwright test --grep-invert=@unsatisfactory", 11 | "test:local": "test_env=local npx playwright test --grep-invert=@unsatisfactory", 12 | "test:happy": "npx playwright test --grep @happy --grep-invert=@unsatisfactory", 13 | "test:unsatisfactory": "npx playwright test --grep=@unsatisfactory", 14 | "ui": "npx playwright test --ui", 15 | "lint": "eslint .", 16 | "lint:fix": "eslint . --fix", 17 | "prettier": "prettier . --check", 18 | "prettier:fix": "prettier . --write", 19 | "prepare": "husky install", 20 | "changed": "npx playwright test --only-changed" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/playwrightsolutions/playwright-api-test-demo.git" 25 | }, 26 | "keywords": [], 27 | "author": "", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/playwrightsolutions/playwright-api-test-demo/issues" 31 | }, 32 | "homepage": "https://github.com/playwrightsolutions/playwright-api-test-demo#readme", 33 | "devDependencies": { 34 | "@currents/playwright": "^0.10.6", 35 | "@faker-js/faker": "^8.2.0", 36 | "@playwright/test": "^1.47.2", 37 | "@types/jest": "^29.5.8", 38 | "@typescript-eslint/eslint-plugin": "^6.10.0", 39 | "@typescript-eslint/parser": "^6.10.0", 40 | "ajv": "^8.12.0", 41 | "eslint": "^8.53.0", 42 | "eslint-config-prettier": "^9.0.0", 43 | "genson-js": "^0.0.8", 44 | "husky": "^8.0.3", 45 | "jest": "^29.7.0", 46 | "prettier": "3.0.3", 47 | "ts-jest": "^29.1.1", 48 | "typescript": "^5.2.2" 49 | }, 50 | "dependencies": { 51 | "dotenv": "^16.3.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@playwright/test"; 2 | import { config } from "dotenv"; 3 | import Env from "@helpers/env"; 4 | 5 | /* This allows you to pass in a `test_env` environment variable 6 | to specify which environment you want to run the tests against */ 7 | if (process.env.test_env) { 8 | config({ 9 | path: `.env.${process.env.test_env}`, 10 | override: true, 11 | }); 12 | } else { 13 | config(); 14 | } 15 | 16 | if (!process.env.CURRENTS_CI_BUILD_ID) { 17 | process.env.CURRENTS_CI_BUILD_ID = "butch-local-" + new Date().getTime(); 18 | } 19 | 20 | export default defineConfig({ 21 | // Keeping this section commented out due to using storage state will make all api calls succeed (even the negative test scenarios) 22 | // projects: [ 23 | // { name: "setup", testMatch: /.*\.setup\.ts/ }, 24 | // { 25 | // name: "api-checks", 26 | // use: { 27 | // storageState: ".auth/admin.json", 28 | // }, 29 | // dependencies: ["setup"], 30 | // }, 31 | // ], 32 | testDir: "tests", 33 | projects: [ 34 | { name: "setup", testMatch: /coverage.setup.ts/, teardown: "teardown" }, 35 | { 36 | name: "api-checks", 37 | dependencies: ["setup"], 38 | }, 39 | { 40 | name: "teardown", 41 | testMatch: /completion.teardown.ts/, 42 | }, 43 | ], 44 | 45 | use: { 46 | extraHTTPHeaders: { 47 | "playwright-solutions": "true", 48 | }, 49 | baseURL: Env.URL, 50 | ignoreHTTPSErrors: true, 51 | trace: "on", 52 | }, 53 | 54 | retries: 2, 55 | reporter: process.env.CI ? [["github"], ["list"], ["html"], ["@currents/playwright"]] : [["list"], ["html"]], 56 | }); 57 | -------------------------------------------------------------------------------- /tests/auth.setup.ts: -------------------------------------------------------------------------------- 1 | // auth.setup.ts 2 | // This is currently not active but can be made active through the playwright.config.ts 3 | // The below is an example of how you can save your storage state to a file in the .auth directory 4 | 5 | import Env from "@helpers/env"; 6 | import { test as setup } from "@playwright/test"; 7 | 8 | const username = Env.ADMIN_NAME; 9 | const password = Env.ADMIN_PASSWORD; 10 | const authFile = ".auth/admin.json"; 11 | 12 | setup("authenticate", async ({ request, baseURL }) => { 13 | await request.post(baseURL + "auth/login", { 14 | data: { 15 | username: username, 16 | password: password, 17 | }, 18 | }); 19 | await request.storageState({ path: authFile }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/auth/login.post.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: POST /auth/login 2 | 3 | import { test, expect } from "@fixtures/fixtures"; 4 | import Env from "@helpers/env"; 5 | 6 | test.describe("auth/login POST requests @auth", async () => { 7 | const username = Env.ADMIN_NAME; 8 | const password = Env.ADMIN_PASSWORD; 9 | 10 | test("POST with valid credentials @happy", async ({ request }) => { 11 | // Calculating Duration 12 | const start = Date.now(); 13 | 14 | const response = await request.post(`auth/login`, { 15 | data: { 16 | username: username, 17 | password: password, 18 | }, 19 | }); 20 | 21 | // Calculating Duration 22 | const end = Date.now(); 23 | const duration = end - start; 24 | 25 | // Asserting Duration 26 | expect(duration).toBeLessThan(1000); 27 | 28 | expect(response.status()).toBe(200); 29 | 30 | const body = await response.text(); 31 | expect(body).toBe(""); 32 | expect(response.headers()["set-cookie"]).toContain("token="); 33 | }); 34 | 35 | test("POST with invalid username and password", async ({ request }) => { 36 | const response = await request.post(`auth/login`, { 37 | data: { 38 | username: "invalidUsername", 39 | password: "invalidPassword", 40 | }, 41 | }); 42 | 43 | expect(response.status()).toBe(403); 44 | 45 | const body = await response.text(); 46 | expect(body).toBe(""); 47 | }); 48 | 49 | test("POST with valid username and invalid password", async ({ request }) => { 50 | const response = await request.post(`auth/login`, { 51 | data: { 52 | username: username, 53 | password: "invalidPassword", 54 | }, 55 | }); 56 | 57 | expect(response.status()).toBe(403); 58 | 59 | const body = await response.text(); 60 | expect(body).toBe(""); 61 | }); 62 | 63 | test("POST with invalid username and valid password", async ({ request }) => { 64 | const response = await request.post(`auth/login`, { 65 | data: { 66 | username: "invalidUsername", 67 | password: password, 68 | }, 69 | }); 70 | 71 | expect(response.status()).toBe(403); 72 | 73 | const body = await response.text(); 74 | expect(body).toBe(""); 75 | }); 76 | 77 | test("POST with no username and valid password", async ({ request }) => { 78 | const response = await request.post(`auth/login`, { 79 | data: { 80 | password: password, 81 | }, 82 | }); 83 | 84 | expect(response.status()).toBe(403); 85 | 86 | const body = await response.text(); 87 | expect(body).toBe(""); 88 | }); 89 | 90 | test("POST with empty body", async ({ request }) => { 91 | const response = await request.post(`auth/login`, { 92 | data: {}, 93 | }); 94 | 95 | expect(response.status()).toBe(403); 96 | 97 | const body = await response.text(); 98 | expect(body).toBe(""); 99 | }); 100 | 101 | test("POST with no body", async ({ request }) => { 102 | const response = await request.post(`auth/login`, {}); 103 | 104 | expect(response.status()).toBe(400); 105 | 106 | const body = await response.json(); 107 | expect(body.timestamp).toBeValidDate(); 108 | expect(body.status).toBe(400); 109 | expect(body.error).toBe("Bad Request"); 110 | expect(body.path).toBe(`/auth/login`); 111 | }); 112 | 113 | test("POST with valid credentials then validate with token @happy", async ({ request }) => { 114 | const response = await request.post(`auth/login`, { 115 | data: { 116 | username: username, 117 | password: password, 118 | }, 119 | }); 120 | 121 | expect(response.status()).toBe(200); 122 | 123 | const body = await response.text(); 124 | expect(body).toBe(""); 125 | const headers = response.headers(); 126 | const tokenString = headers["set-cookie"].split(";")[0]; 127 | const token = tokenString.split("=")[1]; 128 | 129 | const validateResponse = await request.post(`auth/validate`, { 130 | data: { token: token }, 131 | }); 132 | 133 | expect(validateResponse.status()).toBe(200); 134 | 135 | const validateBody = await validateResponse.text(); 136 | expect(validateBody).toBe(""); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /tests/auth/logout.post.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: POST /auth/logout 2 | 3 | import { test, expect } from "@playwright/test"; 4 | import { createToken } from "@datafactory/auth"; 5 | 6 | test.describe("auth/logout POST requests @auth", async () => { 7 | let token; 8 | 9 | test.beforeEach(async () => { 10 | token = await createToken(); 11 | }); 12 | 13 | test("POST with valid token @happy", async ({ request }) => { 14 | const response = await request.post(`auth/logout`, { 15 | data: { token: token }, 16 | }); 17 | 18 | expect(response.status()).toBe(200); 19 | 20 | const body = await response.text(); 21 | expect(body).toBe(""); 22 | }); 23 | 24 | test("POST with token that doesn't exist", async ({ request }) => { 25 | const response = await request.post(`auth/logout`, { 26 | data: { token: "doesntexist" }, 27 | }); 28 | 29 | expect(response.status()).toBe(404); 30 | 31 | const body = await response.text(); 32 | expect(body).toBe(""); 33 | }); 34 | 35 | test("POST with valid token then attempt to validate @happy", async ({ request }) => { 36 | const response = await request.post(`auth/logout`, { 37 | data: { token: token }, 38 | }); 39 | 40 | expect(response.status()).toBe(200); 41 | 42 | const body = await response.text(); 43 | expect(body).toBe(""); 44 | 45 | const validateResponse = await request.post(`auth/validate`, { 46 | data: { token: token }, 47 | }); 48 | 49 | expect(validateResponse.status()).toBe(403); 50 | 51 | const validateBody = await validateResponse.text(); 52 | expect(validateBody).toBe(""); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/auth/validate.post.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: POST /auth/validate 2 | 3 | import { test, expect } from "@fixtures/fixtures"; 4 | import { createToken } from "@datafactory/auth"; 5 | 6 | test.describe("auth/validate POST requests @auth", async () => { 7 | let token; 8 | 9 | test.beforeEach(async () => { 10 | token = await createToken(); 11 | }); 12 | 13 | test("POST with valid token @happy", async ({ request }) => { 14 | const response = await request.post(`auth/validate`, { 15 | data: { token: token }, 16 | }); 17 | 18 | expect(response.status()).toBe(200); 19 | 20 | const body = await response.text(); 21 | expect(body).toBe(""); 22 | }); 23 | 24 | test("POST with token that doesn't exist", async ({ request }) => { 25 | const response = await request.post(`auth/validate`, { 26 | data: { token: "doesntexist" }, 27 | }); 28 | 29 | expect(response.status()).toBe(403); 30 | 31 | const body = await response.text(); 32 | expect(body).toBe(""); 33 | }); 34 | 35 | test("POST with empty token", async ({ request }) => { 36 | const response = await request.post(`auth/validate`, { 37 | data: { token: "" }, 38 | }); 39 | 40 | expect(response.status()).toBe(403); 41 | 42 | const body = await response.text(); 43 | expect(body).toBe(""); 44 | }); 45 | 46 | test("POST with empty body", async ({ request }) => { 47 | const response = await request.post(`auth/validate`, {}); 48 | 49 | expect(response.status()).toBe(400); 50 | 51 | const body = await response.json(); 52 | 53 | expect(body.timestamp).toBeValidDate(); 54 | expect(body.status).toBe(400); 55 | expect(body.error).toBe("Bad Request"); 56 | expect(body.path).toBe(`/auth/validate`); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/booking/booking.delete.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: DELETE /booking/{id} 2 | 3 | import { test, expect } from "@playwright/test"; 4 | import { getBookingSummary, createFutureBooking } from "@datafactory/booking"; 5 | import { createRoom } from "@datafactory/room"; 6 | import { createHeaders } from "@helpers/createHeaders"; 7 | 8 | test.describe("booking/{id} DELETE requests @booking", async () => { 9 | let headers; 10 | let bookingId; 11 | let roomId; 12 | 13 | test.beforeAll(async () => { 14 | headers = await createHeaders(); 15 | }); 16 | 17 | test.beforeEach(async () => { 18 | const room = await createRoom(); 19 | roomId = room.roomid; 20 | const futureBooking = await createFutureBooking(roomId); 21 | bookingId = futureBooking.bookingid; 22 | }); 23 | 24 | test("DELETE booking with specific room id: @happy", async ({ request }) => { 25 | const response = await request.delete(`booking/${bookingId}`, { 26 | headers: headers, 27 | }); 28 | 29 | expect(response.status()).toBe(202); 30 | 31 | const body = await response.text(); 32 | expect(body).toBe(""); 33 | 34 | const getBooking = await getBookingSummary(bookingId); 35 | expect(getBooking.bookings.length).toBe(0); 36 | }); 37 | 38 | test("DELETE booking with an id that doesn't exist", async ({ request }) => { 39 | const response = await request.delete("booking/999999", { 40 | headers: headers, 41 | }); 42 | 43 | expect(response.status()).toBe(404); 44 | 45 | const body = await response.text(); 46 | expect(body).toBe(""); 47 | }); 48 | 49 | test("DELETE booking id without authentication", async ({ request }) => { 50 | const response = await request.delete(`booking/${bookingId}`); 51 | 52 | expect(response.status()).toBe(403); 53 | 54 | const body = await response.text(); 55 | expect(body).toBe(""); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/booking/booking.get.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: GET /booking/ 2 | //COVERAGE_TAG: GET /booking/{id} 3 | //COVERAGE_TAG: GET /booking/summary 4 | 5 | import { test, expect } from "@fixtures/fixtures"; 6 | import { createHeaders, createInvalidHeaders } from "@helpers/createHeaders"; 7 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 8 | import { addWarning } from "@helpers/warnings"; 9 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 10 | 11 | test.describe("booking/ GET requests @booking", async () => { 12 | let headers; 13 | let invalidHeader; 14 | 15 | test.beforeAll(async () => { 16 | headers = await createHeaders(); 17 | invalidHeader = await createInvalidHeaders(); 18 | }); 19 | 20 | test("GET booking summary with specific room id @happy", async ({ request }) => { 21 | const response = await request.get("booking/summary?roomid=1"); 22 | 23 | expect(response.status()).toBe(200); 24 | 25 | const body = await response.json(); 26 | expect(body.bookings.length).toBeGreaterThanOrEqual(1); 27 | 28 | expect(body.bookings[0].bookingDates.checkin).toBeValidDate(); 29 | expect(body.bookings[0].bookingDates.checkout).toBeValidDate(); 30 | 31 | await validateJsonSchema("GET_booking_summary", "booking", body); 32 | await validateAgainstSchema(body.bookings[0].bookingDates, "BookingDates", "booking"); 33 | 34 | await addWarning("This test should be refactored: '" + test.info().title + "' to use custom assertions"); 35 | }); 36 | 37 | test("GET booking summary with specific room id that doesn't exist", async ({ request }) => { 38 | const response = await request.get("booking/summary?roomid=999999"); 39 | 40 | expect(response.status()).toBe(200); 41 | 42 | const body = await response.json(); 43 | expect(body.bookings.length).toBe(0); 44 | }); 45 | 46 | test("GET booking summary with specific room id that is empty", async ({ request }) => { 47 | const response = await request.get("booking/summary?roomid="); 48 | 49 | expect(response.status()).toBe(500); 50 | 51 | const body = await response.json(); 52 | expect(body.timestamp).toBeValidDate(); 53 | expect(body.status).toBe(500); 54 | expect(body.error).toBe("Internal Server Error"); 55 | expect(body.path).toBe("/booking/summary"); 56 | }); 57 | 58 | test("GET all bookings with details @happy", async ({ request }) => { 59 | const response = await request.get("booking/", { 60 | headers: headers, 61 | }); 62 | 63 | expect(response.status()).toBe(200); 64 | 65 | const body = await response.json(); 66 | expect(body.bookings.length).toBeGreaterThanOrEqual(1); 67 | expect(body.bookings[0].bookingid).toBe(1); 68 | expect(body.bookings[0].roomid).toBe(1); 69 | expect(body.bookings[0].firstname).toBe("James"); 70 | expect(body.bookings[0].lastname).toBe("Dean"); 71 | expect(body.bookings[0].depositpaid).toBe(true); 72 | expect(body.bookings[0].bookingdates.checkin).toBeValidDate(); 73 | expect(body.bookings[0].bookingdates.checkout).toBeValidDate(); 74 | 75 | await validateJsonSchema("GET_all_bookings", "booking", body); 76 | await validateAgainstSchema(body.bookings[0], "Booking", "booking", ["email", "phone"]); 77 | }); 78 | 79 | test("GET all bookings with details with no authentication", async ({ request }) => { 80 | const response = await request.get("booking/", { 81 | headers: invalidHeader, 82 | }); 83 | 84 | expect(response.status()).toBe(403); 85 | 86 | const body = await response.text(); 87 | expect(body).toBe(""); 88 | }); 89 | 90 | test("GET booking by id with details", async ({ request }) => { 91 | const response = await request.get("booking/1", { 92 | headers: headers, 93 | }); 94 | 95 | expect(response.status()).toBe(200); 96 | 97 | const body = await response.json(); 98 | expect(body.bookingid).toBe(1); 99 | expect(body.roomid).toBe(1); 100 | expect(body.firstname).toBe("James"); 101 | expect(body.lastname).toBe("Dean"); 102 | expect(body.depositpaid).toBe(true); 103 | expect(body.bookingdates.checkin).toBeValidDate(); 104 | expect(body.bookingdates.checkout).toBeValidDate(); 105 | 106 | await validateJsonSchema("GET_booking_id", "booking", body); 107 | }); 108 | 109 | test("GET booking by id that doesn't exist", async ({ request }) => { 110 | const response = await request.get("booking/999999", { 111 | headers: headers, 112 | }); 113 | 114 | expect(response.status()).toBe(404); 115 | 116 | const body = await response.text(); 117 | expect(body).toBe(""); 118 | }); 119 | 120 | test("GET booking by id without authentication", async ({ request }) => { 121 | const response = await request.get("booking/1"); 122 | 123 | expect(response.status()).toBe(403); 124 | 125 | const body = await response.text(); 126 | expect(body).toBe(""); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /tests/booking/booking.post.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: POST /booking/ 2 | 3 | import { test, expect } from "@playwright/test"; 4 | import { createRandomBookingBody, futureOpenCheckinDate } from "@datafactory/booking"; 5 | import { stringDateByDays } from "@helpers/date"; 6 | import { createRoom } from "@datafactory/room"; 7 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 8 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 9 | 10 | test.describe("booking/ POST requests @booking", async () => { 11 | let requestBody; 12 | let roomId; 13 | 14 | test.beforeEach(async () => { 15 | const room = await createRoom(); 16 | roomId = room.roomid; 17 | 18 | const futureCheckinDate = await futureOpenCheckinDate(roomId); 19 | const checkInString = futureCheckinDate.toISOString().split("T")[0]; 20 | const checkOutString = stringDateByDays(futureCheckinDate, 2); 21 | 22 | requestBody = await createRandomBookingBody(roomId, checkInString, checkOutString); 23 | }); 24 | 25 | test("POST new booking with full body @happy", async ({ request }) => { 26 | const response = await request.post("booking/", { 27 | data: requestBody, 28 | }); 29 | 30 | // if 409 is returned, it means the room is already booked for the dates, will refactor to create a new room to book so we don't get these conflicts 31 | expect(response.status()).toBe(201); 32 | 33 | const body = await response.json(); 34 | expect(body.bookingid).toBeGreaterThan(1); 35 | 36 | const booking = body.booking; 37 | expect(booking.bookingid).toBe(body.bookingid); 38 | expect(booking.roomid).toBe(requestBody.roomid); 39 | expect(booking.firstname).toBe(requestBody.firstname); 40 | expect(booking.lastname).toBe(requestBody.lastname); 41 | expect(booking.depositpaid).toBe(requestBody.depositpaid); 42 | 43 | const bookingdates = booking.bookingdates; 44 | expect(bookingdates.checkin).toBe(requestBody.bookingdates.checkin); 45 | expect(bookingdates.checkout).toBe(requestBody.bookingdates.checkout); 46 | 47 | await validateJsonSchema("POST_booking", "booking", body); 48 | await validateAgainstSchema(booking, "Booking", "booking", ["email", "phone"]); 49 | await validateAgainstSchema(booking.bookingdates, "BookingDates", "booking"); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/booking/booking.put.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: PUT /booking/{id} 2 | 3 | import { test, expect } from "@fixtures/fixtures"; 4 | import { getBookingById, futureOpenCheckinDate, createFutureBooking } from "@datafactory/booking"; 5 | import { stringDateByDays } from "@helpers/date"; 6 | import { createHeaders, createInvalidHeaders } from "@helpers/createHeaders"; 7 | import { createRoom } from "@datafactory/room"; 8 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 9 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 10 | 11 | test.describe("booking/{id} PUT requests @booking", async () => { 12 | let headers; 13 | let invalidHeader; 14 | let bookingId; 15 | let room; 16 | let roomId; 17 | const firstname = "Happy"; 18 | const lastname = "McPathy"; 19 | const depositpaid = false; 20 | const email = "testy@mcpathyson.com"; 21 | const phone = "5555555555555"; 22 | let futureBooking; 23 | let futureCheckinDate; 24 | 25 | test.beforeAll(async () => { 26 | headers = await createHeaders(); 27 | invalidHeader = await createInvalidHeaders(); 28 | }); 29 | 30 | test.beforeEach(async () => { 31 | room = await createRoom("Flaky", 67); 32 | roomId = room.roomid; 33 | futureBooking = await createFutureBooking(roomId); 34 | bookingId = futureBooking.bookingid; 35 | futureCheckinDate = await futureOpenCheckinDate(roomId); 36 | }); 37 | 38 | test(`PUT booking with specific room id @happy`, async ({ request }) => { 39 | const putBody = { 40 | bookingid: bookingId, 41 | roomid: roomId, 42 | firstname: firstname, 43 | lastname: lastname, 44 | depositpaid: depositpaid, 45 | email: email, 46 | phone: phone, 47 | bookingdates: { 48 | checkin: stringDateByDays(futureCheckinDate, 0), 49 | checkout: stringDateByDays(futureCheckinDate, 1), 50 | }, 51 | }; 52 | const response = await request.put(`booking/${bookingId}`, { 53 | headers: headers, 54 | data: putBody, 55 | }); 56 | 57 | expect(response.status()).toBe(200); 58 | 59 | const body = await response.json(); 60 | expect(body.bookingid).toBeGreaterThan(1); 61 | 62 | const booking = body.booking; 63 | expect(booking.bookingid).toBe(bookingId); 64 | expect(booking.roomid).toBe(putBody.roomid); 65 | expect(booking.firstname).toBe(putBody.firstname); 66 | expect(booking.lastname).toBe(putBody.lastname); 67 | expect(booking.depositpaid).toBe(putBody.depositpaid); 68 | 69 | const bookingdates = booking.bookingdates; 70 | expect(bookingdates.checkin).toBe(putBody.bookingdates.checkin); 71 | expect(bookingdates.checkout).toBe(putBody.bookingdates.checkout); 72 | 73 | await validateJsonSchema("PUT_booking_id", "booking", body); 74 | await validateAgainstSchema(booking, "Booking", "booking", ["email", "phone"]); 75 | await validateAgainstSchema(booking.bookingdates, "BookingDates", "booking"); 76 | 77 | await test.step("Verify booking was updated", async () => { 78 | const getBookingBody = await getBookingById(bookingId); 79 | expect(getBookingBody.bookingid).toBeGreaterThan(1); 80 | expect(getBookingBody.bookingid).toBe(bookingId); 81 | expect(getBookingBody.roomid).toBe(putBody.roomid); 82 | expect(getBookingBody.firstname).toBe(putBody.firstname); 83 | expect(getBookingBody.lastname).toBe(putBody.lastname); 84 | expect(getBookingBody.depositpaid).toBe(putBody.depositpaid); 85 | 86 | const getBookingDates = getBookingBody.bookingdates; 87 | expect(getBookingDates.checkin).toBe(putBody.bookingdates.checkin); 88 | expect(getBookingDates.checkout).toBe(putBody.bookingdates.checkout); 89 | }); 90 | }); 91 | 92 | test("PUT booking without firstname in putBody", async ({ request }) => { 93 | const putBody = { 94 | bookingid: bookingId, 95 | roomid: roomId, 96 | lastname: lastname, 97 | depositpaid: depositpaid, 98 | email: email, 99 | phone: phone, 100 | bookingdates: { 101 | checkin: stringDateByDays(futureCheckinDate, 0), 102 | checkout: stringDateByDays(futureCheckinDate, 1), 103 | }, 104 | }; 105 | const response = await request.put(`booking/${bookingId}`, { 106 | headers: headers, 107 | data: putBody, 108 | }); 109 | 110 | expect(response.status()).toBe(400); 111 | 112 | const body = await response.json(); 113 | expect(body.error).toBe("BAD_REQUEST"); 114 | expect(body.errorCode).toBe(400); 115 | expect(body.errorMessage).toContain( 116 | "Validation failed for argument [0] in public org.springframework.http.ResponseEntity" 117 | ); 118 | expect(body.fieldErrors[0]).toBe("Firstname should not be blank"); 119 | }); 120 | 121 | test("PUT booking with an id that doesn't exist", async ({ request }) => { 122 | const putBody = { 123 | bookingid: bookingId, 124 | roomid: roomId, 125 | firstname: firstname, 126 | lastname: lastname, 127 | depositpaid: depositpaid, 128 | email: email, 129 | phone: phone, 130 | bookingdates: { 131 | checkin: stringDateByDays(futureCheckinDate, 0), 132 | checkout: stringDateByDays(futureCheckinDate, 1), 133 | }, 134 | }; 135 | 136 | const response = await request.delete("booking/999999", { 137 | headers: headers, 138 | data: putBody, 139 | }); 140 | 141 | expect(response.status()).toBe(404); 142 | 143 | const body = await response.text(); 144 | expect(body).toBe(""); 145 | }); 146 | 147 | test(`PUT booking id that is text`, async ({ request }) => { 148 | const putBody = { 149 | bookingid: bookingId, 150 | roomid: roomId, 151 | firstname: firstname, 152 | lastname: lastname, 153 | depositpaid: depositpaid, 154 | email: email, 155 | phone: phone, 156 | bookingdates: { 157 | checkin: stringDateByDays(futureCheckinDate, 0), 158 | checkout: stringDateByDays(futureCheckinDate, 1), 159 | }, 160 | }; 161 | 162 | const response = await request.put(`booking/asdf`, { 163 | headers: headers, 164 | data: putBody, 165 | }); 166 | 167 | expect(response.status()).toBe(404); 168 | 169 | const body = await response.json(); 170 | expect(body.timestamp).toBeValidDate(); 171 | expect(body.status).toBe(404); 172 | expect(body.error).toBe("Not Found"); 173 | expect(body.path).toBe("/booking/asdf"); 174 | }); 175 | 176 | test("PUT booking id with invalid authentication", async ({ request }) => { 177 | const putBody = { 178 | bookingid: bookingId, 179 | roomid: roomId, 180 | firstname: firstname, 181 | lastname: lastname, 182 | depositpaid: depositpaid, 183 | email: email, 184 | phone: phone, 185 | bookingdates: { 186 | checkin: stringDateByDays(futureCheckinDate, 0), 187 | checkout: stringDateByDays(futureCheckinDate, 1), 188 | }, 189 | }; 190 | 191 | const response = await request.put(`booking/${bookingId}`, { 192 | headers: invalidHeader, 193 | data: putBody, 194 | }); 195 | 196 | expect(response.status()).toBe(403); 197 | 198 | const body = await response.text(); 199 | expect(body).toBe(""); 200 | }); 201 | 202 | test("PUT booking id without authentication", async ({ request }) => { 203 | const putBody = { 204 | bookingid: bookingId, 205 | roomid: roomId, 206 | firstname: firstname, 207 | lastname: lastname, 208 | depositpaid: depositpaid, 209 | email: email, 210 | phone: phone, 211 | bookingdates: { 212 | checkin: stringDateByDays(futureCheckinDate, 0), 213 | checkout: stringDateByDays(futureCheckinDate, 1), 214 | }, 215 | }; 216 | 217 | const response = await request.put(`booking/${bookingId}`, { 218 | data: putBody, 219 | }); 220 | 221 | expect(response.status()).toBe(403); 222 | 223 | const body = await response.text(); 224 | expect(body).toBe(""); 225 | }); 226 | 227 | test("PUT booking id without put body", async ({ request }) => { 228 | const response = await request.put(`booking/${bookingId}`, { 229 | headers: headers, 230 | }); 231 | 232 | expect(response.status()).toBe(400); 233 | 234 | const body = await response.json(); 235 | expect(body.timestamp).toBeValidDate(); 236 | expect(body.status).toBe(400); 237 | expect(body.error).toBe("Bad Request"); 238 | expect(body.path).toBe(`/booking/${bookingId}`); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /tests/branding/branding.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: GET /branding/ 2 | //COVERAGE_TAG: PUT /branding/ 3 | 4 | import { test, expect } from "@playwright/test"; 5 | import { defaultBranding, defaultBrandingShortLogo, updatedBranding } from "@helpers/branding"; 6 | import { createHeaders } from "@helpers/createHeaders"; 7 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 8 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 9 | 10 | test.describe("branding/ GET requests @branding", async () => { 11 | const defaultBodyShort = defaultBrandingShortLogo; 12 | 13 | test("GET website branding @happy", async ({ request }) => { 14 | const response = await request.get("branding"); 15 | 16 | expect(response.status()).toBe(200); 17 | const body = await response.json(); 18 | expect(body).toEqual(defaultBodyShort); 19 | 20 | await validateJsonSchema("GET_branding", "branding", body); 21 | await validateAgainstSchema(body, "Branding", "branding"); 22 | await validateAgainstSchema(body.contact, "Contact", "branding"); 23 | await validateAgainstSchema(body.map, "Map", "branding"); 24 | }); 25 | }); 26 | 27 | // This test has the potential to cause other UI tests to fail as this branding endpoint is a critical part of the entire UI of the website 28 | test.describe("branding/ PUT requests", async () => { 29 | const defaultBody = defaultBranding; 30 | const updatedBody = updatedBranding; 31 | let headers; 32 | 33 | test.beforeAll(async () => { 34 | headers = await createHeaders(); 35 | }); 36 | 37 | test.afterEach(async ({ request }) => { 38 | await request.put("branding/", { 39 | headers: headers, 40 | data: defaultBody, 41 | }); 42 | }); 43 | 44 | test("PUT website branding", async ({ request }) => { 45 | const response = await request.put("branding/", { 46 | headers: headers, 47 | data: updatedBody, 48 | }); 49 | 50 | expect(response.status()).toBe(202); 51 | const body = await response.json(); 52 | expect(body).toEqual(updatedBody); 53 | 54 | await validateJsonSchema("PUT_branding", "branding", body); 55 | await validateAgainstSchema(body, "Branding", "branding"); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/completion.teardown.ts: -------------------------------------------------------------------------------- 1 | import { warningsFile } from "@helpers/warnings"; 2 | import { test as teardown } from "@playwright/test"; 3 | import * as fs from "fs"; 4 | 5 | teardown("Print warnings", async () => { 6 | // read the file and console.log the result 7 | try { 8 | const data = fs.readFileSync(warningsFile, "utf8"); 9 | console.log(data); 10 | } catch (err) { 11 | null; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /tests/coverage.setup.ts: -------------------------------------------------------------------------------- 1 | import { getEndpointCoverage } from "../lib/helpers/coverage"; 2 | import { test as coverage } from "@playwright/test"; 3 | import * as fs from "fs"; 4 | import { warningsFile } from "@helpers/warnings"; 5 | import { updateDocsSchemasParamsCount } from "@helpers/validateAgainstSchema"; 6 | 7 | coverage("calculate coverage", async () => { 8 | await getEndpointCoverage("auth"); 9 | await getEndpointCoverage("booking"); 10 | await getEndpointCoverage("room"); 11 | await getEndpointCoverage("branding"); 12 | await getEndpointCoverage("report"); 13 | await getEndpointCoverage("message"); 14 | 15 | // delete a warnings file if exists 16 | if (fs.existsSync(warningsFile)) { 17 | try { 18 | await fs.promises.unlink(warningsFile); 19 | } catch (err) { 20 | console.error(err); 21 | } 22 | } 23 | 24 | // function to generate new files to track documentation objects and their parameters count 25 | const generateNewFiles = process.env.GENERATE_SCHEMA_TRACKING_DATA; 26 | if (generateNewFiles === "true") { 27 | await updateDocsSchemasParamsCount(); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /tests/message/message.delete.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: DELETE /message/{id} 2 | 3 | import { createMessage } from "@datafactory/message"; 4 | import { createHeaders } from "@helpers/createHeaders"; 5 | import { test, expect } from "@playwright/test"; 6 | 7 | test.describe("message/ DELETE requests @message", async () => { 8 | let message; 9 | let authHeaders; 10 | 11 | test.beforeEach(async () => { 12 | message = await createMessage(); 13 | authHeaders = await createHeaders(); 14 | }); 15 | 16 | test("DELETE a message", async ({ request }) => { 17 | const response = await request.delete(`/message/${message.messageid}`, { 18 | headers: authHeaders, 19 | data: "", 20 | }); 21 | 22 | expect(response.status()).toBe(202); 23 | const body = await response.text(); 24 | expect(body).toEqual(""); 25 | 26 | //check the message again and ensure it's not available 27 | const response2 = await request.get(`/message/${message.messageid}`); 28 | expect(response2.status()).toBe(500); 29 | const body2 = await response2.json(); 30 | expect(body2.error).toEqual("Internal Server Error"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/message/message.get.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: GET /message/ 2 | //COVERAGE_TAG: GET /message/{id} 3 | //COVERAGE_TAG: GET /message/count 4 | 5 | import { test, expect } from "@fixtures/fixtures"; 6 | import { createMessage } from "@datafactory/message"; 7 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 8 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 9 | 10 | test.describe("message/ GET requests @message", async () => { 11 | let message; 12 | 13 | test.beforeEach(async () => { 14 | message = await createMessage(); 15 | }); 16 | 17 | test("GET all messages @happy", async ({ request }) => { 18 | const response = await request.get("/message/"); 19 | 20 | expect(response.status()).toBe(200); 21 | const body = await response.json(); 22 | expect(body.messages[0]).toMatchObject({ 23 | id: 1, 24 | name: "James Dean", 25 | subject: "Booking enquiry", 26 | read: false, 27 | }); 28 | 29 | await validateJsonSchema("GET_message", "message", body); 30 | await validateAgainstSchema(body, "Messages", "message"); 31 | }); 32 | 33 | test("GET a message by id @happy", async ({ request }) => { 34 | const response = await request.get(`/message/${message.messageid}`); 35 | 36 | expect(response.status()).toBe(200); 37 | const body = await response.json(); 38 | expect(body).toEqual(message); 39 | 40 | await validateJsonSchema("GET_message_id", "message", body); 41 | await validateAgainstSchema(body, "Message", "message"); 42 | }); 43 | 44 | test("GET current message count @happy", async ({ request }) => { 45 | const response = await request.get("/message/count"); 46 | 47 | expect(response.status()).toBe(200); 48 | const body = await response.json(); 49 | expect(body.count).toBeNumber(); 50 | expect(body.count).toBeGreaterThanOrEqual(1); 51 | 52 | await validateJsonSchema("GET_message_count", "message", body); 53 | await validateAgainstSchema(body, "Count", "message"); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/message/message.post.spec.ts: -------------------------------------------------------------------------------- 1 | // COVERAGE_TAG: POST /message/ 2 | 3 | import { newMessageBody } from "@datafactory/message"; 4 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 5 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 6 | import { test, expect } from "@playwright/test"; 7 | 8 | test.describe("message/ POST requests @message", async () => { 9 | const message = await newMessageBody(); 10 | 11 | test("POST a message @happy", async ({ request }) => { 12 | const response = await request.post("/message/", { 13 | data: message, 14 | }); 15 | 16 | expect(response.status()).toBe(201); 17 | const body = await response.json(); 18 | expect(body.messageid).toBeGreaterThan(1); 19 | expect(body.name).toBe(message.name); 20 | expect(body.email).toBe(message.email); 21 | expect(body.phone).toBe(message.phone); 22 | expect(body.subject).toBe(message.subject); 23 | expect(body.description).toBe(message.description); 24 | 25 | await validateJsonSchema("POST_message", "message", body); 26 | await validateAgainstSchema(body, "Message", "message"); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/message/message.put.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: PUT /message/{id}/read 2 | 3 | import { createMessage } from "@datafactory/message"; 4 | import { createHeaders } from "@helpers/createHeaders"; 5 | import { test, expect } from "@playwright/test"; 6 | 7 | test.describe("message/ PUT requests @message", async () => { 8 | let message; 9 | let authHeaders; 10 | 11 | test.beforeEach(async () => { 12 | message = await createMessage(); 13 | authHeaders = await createHeaders(); 14 | }); 15 | 16 | test("PUT a message as read @happy", async ({ request }) => { 17 | const response = await request.put(`/message/${message.messageid}/read`, { 18 | headers: authHeaders, 19 | data: "", 20 | }); 21 | 22 | expect(response.status()).toBe(202); 23 | const body = await response.text(); 24 | expect(body).toEqual(""); 25 | 26 | //get the message again and check it's read 27 | const response2 = await request.get("/message/"); 28 | expect(response2.status()).toBe(200); 29 | const body2 = await response2.json(); 30 | 31 | const match = body2.messages.find((item) => item.id === message.messageid); 32 | 33 | expect(match).toBeDefined(); 34 | expect(match.read).toBe(true); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/report/report.get.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: GET /report/ 2 | //COVERAGE_TAG: GET /report/room/{id} 3 | 4 | import { createFutureBooking } from "@datafactory/booking"; 5 | import { createRoom } from "@datafactory/room"; 6 | import { createHeaders } from "@helpers/createHeaders"; 7 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 8 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 9 | import { test, expect } from "@fixtures/fixtures"; 10 | 11 | test.describe("report/ GET requests @report", async () => { 12 | let headers; 13 | let room; 14 | 15 | test.beforeEach(async () => { 16 | headers = await createHeaders(); 17 | room = await createRoom(); 18 | await createFutureBooking(room.roomid); 19 | }); 20 | 21 | test("GET a report @happy", async ({ request }) => { 22 | const response = await request.get("/report/", { 23 | headers: headers, 24 | }); 25 | 26 | expect(response.status()).toBe(200); 27 | const body = await response.json(); 28 | expect(body.report.length).toBeGreaterThanOrEqual(1); 29 | 30 | // Leaving this here for demo purposes 31 | // createAssertions(body, "body"); 32 | 33 | // I am asserting on each booking in the report array 34 | body.report.forEach((booking) => { 35 | expect(booking.start).toBeValidDate(); 36 | expect(booking.end).toBeValidDate(); 37 | expect(booking.title).toBeString(); 38 | }); 39 | 40 | await validateJsonSchema("GET_report", "report", body); 41 | await validateAgainstSchema(body, "Report", "report"); 42 | }); 43 | 44 | test("GET room report by id @happy", async ({ request }) => { 45 | const response = await request.get(`/report/room/${room.roomid}`); 46 | 47 | expect(response.status()).toBe(200); 48 | const body = await response.json(); 49 | expect(body.report.length).toBeGreaterThan(0); 50 | expect(body.report[0].start).toBeValidDate(); 51 | expect(body.report[0].end).toBeValidDate(); 52 | expect(body.report[0].title).toBe("Unavailable"); 53 | 54 | await validateJsonSchema("GET_report_room_id", "report", body); 55 | await validateAgainstSchema(body, "Report", "report"); 56 | await validateAgainstSchema(body.report[0], "Entry", "report"); //redundant but helpful as an example 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/room/room.delete.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: DELETE /room/{id} 2 | 3 | import { createRoom } from "@datafactory/room"; 4 | import { createHeaders } from "@helpers/createHeaders"; 5 | import { test, expect } from "@playwright/test"; 6 | 7 | test.describe("room/ DELETE requests @room", async () => { 8 | let room; 9 | let roomId; 10 | let authHeaders; 11 | 12 | test.beforeEach(async () => { 13 | room = await createRoom("DELETE", 50); 14 | roomId = room.roomid; 15 | authHeaders = await createHeaders(); 16 | }); 17 | 18 | test("DELETE a room", async ({ request }) => { 19 | const response = await request.delete(`/room/${roomId}`, { 20 | headers: authHeaders, 21 | data: "", 22 | }); 23 | 24 | expect(response.status()).toBe(202); 25 | const body = await response.text(); 26 | expect(body).toEqual(""); 27 | 28 | //check the message again and ensure it's not available 29 | const response2 = await request.get(`/room/${roomId}`); 30 | expect(response2.status()).toBe(500); 31 | const body2 = await response2.json(); 32 | expect(body2.error).toEqual("Internal Server Error"); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/room/room.get.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: GET /room/ 2 | //COVERAGE_TAG: GET /room/{id} 3 | 4 | import { createRoom, defaultRoom } from "@datafactory/room"; 5 | import { createAssertions } from "@helpers/createAssertions"; // eslint-disable-line 6 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 7 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 8 | import { test, expect } from "@fixtures/fixtures"; 9 | 10 | test.describe("room/ GET requests @room", async () => { 11 | let room; 12 | let roomId; 13 | 14 | test.beforeEach(async () => { 15 | room = await createRoom("GET", 50); 16 | roomId = room.roomid; 17 | }); 18 | 19 | test("GET all rooms @happy", async ({ request }) => { 20 | const response = await request.get("/room/"); 21 | 22 | expect(response.status()).toBe(200); 23 | const body = await response.json(); 24 | 25 | // Find the room object with created RoomId out if array of rooms to run deeper assertions against 26 | let roomToAssertAgainst; 27 | 28 | for (const room of body.rooms) { 29 | if (room.roomid === roomId) { 30 | roomToAssertAgainst = room; 31 | break; 32 | } 33 | } 34 | 35 | expect(roomToAssertAgainst).toMatchObject(room); 36 | 37 | // get first room in the array and assert against the default room information 38 | const firstRoom = body.rooms[0]; 39 | expect(firstRoom).toMatchObject(defaultRoom); 40 | 41 | // this set of assertions is doing the same thing as the expect above 42 | expect(firstRoom.roomid).toBe(1); 43 | expect(firstRoom.roomName).toBe("101"); 44 | expect(firstRoom.type).toBe("single"); 45 | expect(firstRoom.image).toBe("/images/room2.jpg"); 46 | expect(firstRoom.description).toBe( 47 | "Aenean porttitor mauris sit amet lacinia molestie. In posuere accumsan aliquet. Maecenas sit amet nisl massa. Interdum et malesuada fames ac ante." 48 | ); 49 | expect(firstRoom.features).toEqual(["TV", "WiFi", "Safe"]); 50 | expect(firstRoom.roomPrice).toBe(100); 51 | 52 | // We loop through each room in the array and assert against the type of each property 53 | body.rooms.forEach((room) => { 54 | expect(room.roomid).toBeNumber(); 55 | expect(room.roomName).toBeString(); 56 | expect(room.type).toBeString(); 57 | expect(room.image).toBeString(); 58 | expect(room.description).toBeString(); 59 | expect(room.features).toBeObject(); 60 | expect(room.roomPrice).toBeNumber(); 61 | }); 62 | 63 | await validateJsonSchema("GET_room", "room", body); 64 | await validateAgainstSchema(body, "Rooms", "room"); 65 | }); 66 | 67 | test("GET a room by id @happy", async ({ request }) => { 68 | const response = await request.get(`/room/${roomId}`); 69 | 70 | expect(response.status()).toBe(200); 71 | const body = await response.json(); 72 | expect(body).toEqual(room); 73 | 74 | await validateJsonSchema("GET_room_id", "room", body); 75 | await validateAgainstSchema(body, "Room", "room"); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/room/room.post.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: POST /room/ 2 | 3 | import { createRandomRoomBody } from "@datafactory/room"; 4 | import { createHeaders } from "@helpers/createHeaders"; 5 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 6 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 7 | import { test, expect } from "@fixtures/fixtures"; 8 | 9 | test.describe("room/ POST requests @room", async () => { 10 | let authHeaders; 11 | let updateRoomBody; 12 | 13 | test.beforeEach(async () => { 14 | authHeaders = await createHeaders(); 15 | updateRoomBody = await createRandomRoomBody(); 16 | }); 17 | 18 | test("POST /room to create a room @happy", async ({ request }) => { 19 | const response = await request.post(`/room/`, { 20 | headers: authHeaders, 21 | data: updateRoomBody, 22 | }); 23 | 24 | expect(response.status()).toBe(201); 25 | const body = await response.json(); 26 | 27 | expect(body.roomid).toBeNumber(); 28 | expect(body.name).toEqual(updateRoomBody.name); 29 | expect(body.accessible).toEqual(updateRoomBody.accessible); 30 | expect(body.description).toEqual(updateRoomBody.description); 31 | expect(body.features).toEqual(updateRoomBody.features); 32 | expect(body.image).toEqual(updateRoomBody.image); 33 | expect(body.roomName).toEqual(updateRoomBody.roomName); 34 | expect(body.type).toEqual(updateRoomBody.type); 35 | 36 | await validateJsonSchema("POST_room", "room", body); 37 | await validateAgainstSchema(body, "Room", "room"); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/room/room.put.spec.ts: -------------------------------------------------------------------------------- 1 | //COVERAGE_TAG: PUT /room/{id} 2 | 3 | import { createRoom, createRandomRoomBody } from "@datafactory/room"; 4 | import { createHeaders } from "@helpers/createHeaders"; 5 | import { randomRoomFeaturesCount } from "@helpers/roomFeatures"; 6 | import { validateAgainstSchema } from "@helpers/validateAgainstSchema"; 7 | import { validateJsonSchema } from "@helpers/validateJsonSchema"; 8 | import { test, expect } from "@playwright/test"; 9 | 10 | test.describe("room/ PUT requests @room", async () => { 11 | let room; 12 | let roomId; 13 | let authHeaders; 14 | let updateRoomBody; 15 | 16 | test.beforeEach(async () => { 17 | room = await createRoom("PUT", 50); 18 | roomId = room.roomid; 19 | authHeaders = await createHeaders(); 20 | updateRoomBody = await createRandomRoomBody(); 21 | }); 22 | 23 | test("PUT /room to update values", async ({ request }) => { 24 | const response = await request.put(`/room/${roomId}`, { 25 | headers: authHeaders, 26 | data: updateRoomBody, 27 | }); 28 | 29 | expect(response.status()).toBe(202); 30 | const body = await response.json(); 31 | 32 | expect(body.roomid).toEqual(roomId); 33 | expect(body.name).toEqual(updateRoomBody.name); 34 | expect(body.accessible).toEqual(updateRoomBody.accessible); 35 | expect(body.description).toEqual(updateRoomBody.description); 36 | expect(body.features).toEqual(updateRoomBody.features); 37 | expect(body.image).toEqual(updateRoomBody.image); 38 | expect(body.roomName).toEqual(updateRoomBody.roomName); 39 | expect(body.type).toEqual(updateRoomBody.type); 40 | 41 | await validateJsonSchema("PUT_room_id", "room", body); 42 | await validateAgainstSchema(body, "Room", "room"); 43 | }); 44 | 45 | test("PUT /room to update features @happy", async ({ request }) => { 46 | const randomFeatures = randomRoomFeaturesCount(10); 47 | 48 | // Overwrites the features array with random features 49 | updateRoomBody.features = randomFeatures; 50 | 51 | const response = await request.put(`/room/${roomId}`, { 52 | headers: authHeaders, 53 | data: updateRoomBody, 54 | }); 55 | 56 | expect(response.status()).toBe(202); 57 | const body = await response.json(); 58 | 59 | expect(body.roomid).toEqual(roomId); 60 | expect(body.name).toEqual(updateRoomBody.name); 61 | expect(body.accessible).toEqual(updateRoomBody.accessible); 62 | expect(body.description).toEqual(updateRoomBody.description); 63 | expect(body.features).toEqual(randomFeatures); 64 | expect(body.image).toEqual(updateRoomBody.image); 65 | expect(body.roomName).toEqual(updateRoomBody.roomName); 66 | expect(body.type).toEqual(updateRoomBody.type); 67 | 68 | await validateJsonSchema("PUT_room_id", "room", body); 69 | await validateAgainstSchema(body, "Room", "room"); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/test.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@fixtures/fixtures"; // Import the custom matchers definition 2 | 3 | test.describe("Custom Assertions", async () => { 4 | test("with fixtures", async ({ request }) => { 5 | const response = await request.post(`auth/login`, {}); 6 | 7 | expect(response.status()).toBe(400); 8 | 9 | const body = await response.json(); 10 | expect(body.timestamp).toBeValidDate(); 11 | 12 | const dateStr = "2021-01-01"; 13 | expect(dateStr).toBeValidDate(); 14 | 15 | const number = 123; 16 | expect(number).toBeNumber(); 17 | 18 | const boolean = true; 19 | expect(boolean).toBeBoolean(); 20 | 21 | const string = "string"; 22 | expect(string).toBeString(); 23 | 24 | expect(body.status).toBeOneOfValues([400, 401, 403]); 25 | expect(body.status).toBeOneOfTypes(["number", "null"]); 26 | }); 27 | 28 | test("flakey test @unsatisfactory", async ({ request }) => { 29 | await request.post(`auth/login`, {}); 30 | 31 | const randomBoolean = Math.random() > 0.5; 32 | expect(randomBoolean).toBe(true); 33 | }); 34 | 35 | test("1 flakey test @happy @unsatisfactory", async ({ request }) => { 36 | await request.post(`auth/login`, {}); 37 | 38 | const randomBoolean = Math.random() > 0.5; 39 | expect(randomBoolean).toBe(true); 40 | }); 41 | 42 | test("2 flakey test @unsatisfactory", async ({ request }) => { 43 | await request.post(`auth/login`, {}); 44 | 45 | const randomBoolean = Math.random() > 0.5; 46 | expect(randomBoolean).toBe(true); 47 | }); 48 | 49 | test("3 flakey test @unsatisfactory", async ({ request }) => { 50 | await request.post(`auth/login`, {}); 51 | 52 | const randomBoolean = Math.random() > 0.5; 53 | expect(randomBoolean).toBe(true); 54 | }); 55 | 56 | test("4 flakey test @unsatisfactory", async ({ request }) => { 57 | await request.post(`auth/login`, {}); 58 | 59 | const randomBoolean = Math.random() > 0.5; 60 | expect(randomBoolean).toBe(true); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "esModuleInterop": true, 5 | "paths": { 6 | "@datafactory/*": ["lib/datafactory/*"], 7 | "@helpers/*": ["lib/helpers/*"], 8 | "@fixtures/*": ["lib/fixtures/*"] 9 | } 10 | } 11 | } 12 | --------------------------------------------------------------------------------