├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ ├── bug.md │ └── feature.md └── workflows │ ├── format-java-code.yml │ ├── reusable-workflow-to-run-tests.yml │ ├── trigger-health-check-on-a-schedule.yml │ ├── trigger-new-updated-and-unit-tests-on-pull-request.yml │ ├── trigger-smoke-tests-on-merge-to-main.yml │ ├── trigger-specific-targetted-tests-on-an-external-event.yml │ └── trigger-tests-manually.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── README.md ├── docker-compose.yml ├── docs ├── README-CODE-FORMATTING.md └── README-GIT-CRYPT.md ├── drawings ├── api-test-framework-design.drawio ├── end-to-end-test-workflow.drawio └── github-pr-workflow.drawio ├── git-crypt-key-restpro ├── images ├── api-test-framework-design.png ├── end-to-end-test-workflow.png └── github-pr-workflow.png ├── pom.xml ├── restpro.iml └── src ├── main ├── java │ └── org │ │ └── powertester │ │ ├── annotations │ │ ├── CsvTest.java │ │ ├── FailingTest.java │ │ ├── FlakyTest.java │ │ ├── HealthCheckTest.java │ │ ├── RegressionTest.java │ │ ├── SmokeTest.java │ │ └── UnitTest.java │ │ ├── auth │ │ ├── AuthBody.java │ │ ├── Scope.java │ │ └── TokenFactory.java │ │ ├── basespec │ │ └── SpecFactory.java │ │ ├── booking │ │ ├── Booking.java │ │ ├── BookingAPI.java │ │ ├── BookingResponse.java │ │ └── entitites │ │ │ └── Bookingdates.java │ │ ├── config │ │ ├── TestConfig.java │ │ └── TestEnv.java │ │ ├── data │ │ └── TestData.java │ │ ├── database │ │ └── DBConnection.java │ │ ├── extensions │ │ ├── LoggingExtension.java │ │ ├── ReportingExtension.java │ │ ├── TestRunExtension.java │ │ ├── TimingExtension.java │ │ └── report │ │ │ ├── ElasticLowLevelRestClientFactory.java │ │ │ ├── ElasticServerChoices.java │ │ │ ├── PublishResults.java │ │ │ └── TestRunMetaData.java │ │ ├── healthcheck │ │ └── HealthCheckAPI.java │ │ └── utils │ │ └── DateUtils.java └── resources │ ├── META-INF │ └── services │ │ └── org.junit.jupiter.api.extension.Extension │ ├── application.conf │ ├── choices.conf │ ├── common │ └── secrets.conf │ ├── develop │ ├── secrets.conf │ ├── test-data.conf │ └── user-info.conf │ ├── junit-platform.properties │ ├── localhost │ ├── secrets.conf │ ├── test-data.conf │ └── user-info.conf │ ├── logback.xml │ ├── schemas │ ├── create-booking-schema.json │ └── read-update-booking-schema.json │ └── staging │ ├── secrets.conf │ ├── test-data.conf │ └── user-info.conf └── test ├── java ├── DoNotUseRestAssuredOnlyTests.java ├── asserts │ ├── ValidateDB.java │ └── VerifyResponse.java ├── booking │ ├── BookingTests.java │ ├── VerifyBookingResponse.java │ └── explore-booking-api.http ├── env │ └── http-client.env.json ├── healthcheck │ └── HealthCheckTests.java └── unittests │ └── CSVUnitTests.java └── resources └── testdata └── data.csv /.gitattributes: -------------------------------------------------------------------------------- 1 | # Uncomment the following line to enable git-crypt (if you wish to encrypt secrets) 2 | # secrets.conf filter=git-crypt diff=git-crypt 3 | 4 | # Prevent encrypting .gitattributes file : 5 | .gitattributes !filter !diff 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | # Title 2 | Replace this text with title. 3 | 4 | # Description 5 | Replace this text with description. 6 | 7 | # Steps to reproduce 8 | 1. Step 1: 9 | 2. Step 2: 10 | 3. so on.. 11 | 12 | # Result (Expected vs Actual) 13 | Replace this text with what was expected vs what was found. 14 | 15 | # Attachment 16 | Add attachments to the ticket. 17 | 18 | # Ticket ID 19 | - [Bug_ID](Replace this text with bug ticket url.) 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | # Title 2 | Replace this text with title. 3 | 4 | # Description 5 | Replace this text with description. 6 | 7 | # Ticket ID 8 | - [Ticket_ID](Replace this text with url.) 9 | -------------------------------------------------------------------------------- /.github/workflows/format-java-code.yml: -------------------------------------------------------------------------------- 1 | name: Format Java Code as per Google Java Format program 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | 9 | formatting: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 # v2 minimum required 13 | - uses: axel-op/googlejavaformat-action@v3 14 | with: 15 | args: "--skip-sorting-imports --replace" 16 | skip-commit: true 17 | 18 | - name: Print diffs 19 | run: | 20 | echo "Please read the README.md file and install pre-commit hooks to properly format the files and resolve this error" 21 | git --no-pager diff --exit-code 22 | -------------------------------------------------------------------------------- /.github/workflows/reusable-workflow-to-run-tests.yml: -------------------------------------------------------------------------------- 1 | name: reusable-workflow-to-run-tests 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | test-environment: 7 | required: false 8 | type: string 9 | default: "STAGING" 10 | 11 | tags-of-tests-to-include: 12 | required: true 13 | type: string 14 | 15 | tags-of-tests-to-exclude: 16 | required: false 17 | type: string 18 | default: "flaky, failing" 19 | 20 | test-files-to-include: 21 | required: false 22 | type: string 23 | default: "" 24 | 25 | run-name: 26 | required: false 27 | type: string 28 | default: "CI" 29 | 30 | jobs: 31 | run-tests: 32 | 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | 38 | - name: Set up JDK 17 39 | uses: actions/setup-java@v3 40 | with: 41 | java-version: '17' 42 | distribution: 'temurin' 43 | cache: maven 44 | 45 | # Uncomment the following lines if you are using git-crypt to encrypt secrets 46 | # - name: Unlock secrets 47 | # uses: sliteteam/github-action-git-crypt-unlock@1.2.0 48 | # env: 49 | # GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }} 50 | 51 | - name: Test with Maven 52 | run: > 53 | mvn 54 | -DTEST_ENV="${{ inputs.test-environment }}" 55 | -Dgroups="${{ inputs.tags-of-tests-to-include }}" 56 | -Dtest="${{ inputs.test-files-to-include }}" 57 | -DexcludedGroups="${{ inputs.tags-of-tests-to-exclude }}" 58 | -DTRIGGERED_BY="${{ github.event_name }}" 59 | -DRUN_NAME="${{ inputs.run-name }}" 60 | -B package --file pom.xml 61 | -------------------------------------------------------------------------------- /.github/workflows/trigger-health-check-on-a-schedule.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: trigger-tests-on-a-schedule 5 | 6 | on: 7 | schedule: 8 | # * is a special character in YAML so you have to quote this string 9 | # To set a new schedule use this: https://crontab.guru/ 10 | # To get the right time for your timezone use : https://www.worldtimebuddy.com/ 11 | - cron: "0 * * * MON-FRI" # “At every hour on every week day. ” 12 | 13 | jobs: 14 | trigger-tests-on-a-schedule: 15 | uses: ./.github/workflows/reusable-workflow-to-run-tests.yml 16 | with: 17 | tags-of-tests-to-include: "healthcheck" # -Dgroups="" means execute all test cases 18 | tags-of-tests-to-exclude: "" # -DexcludedGroups="" means don't exclude any test case 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/trigger-new-updated-and-unit-tests-on-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: trigger-tests-on-a-pull-request 5 | 6 | on: 7 | pull_request: 8 | branches: [ main ] 9 | 10 | # Allows us to run this workflow manually as well from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | touched-test-files: 15 | runs-on: ubuntu-latest # windows-latest || macos-latest 16 | name: Touched test files 17 | permissions: 18 | pull-requests: read 19 | outputs: 20 | test_files_to_run: ${{ steps.list_touched_files.outputs.test_files }} 21 | steps: 22 | - name: Fetch changed files 23 | id: changed-files 24 | uses: tj-actions/changed-files@v44 25 | with: 26 | # Avoid using single or double quotes for multiline patterns 27 | files: src/test/**/*.java 28 | 29 | - name: List changed test files 30 | id: list_touched_files 31 | env: 32 | ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_and_modified_files }} 33 | run: | 34 | tests="" # Initialize the empty variable. If passed empty, this will have no effect on the command. 35 | for file in ${ALL_CHANGED_FILES}; do 36 | filename=$(basename "$file") 37 | echo "$filename was changed" 38 | if [ -z "$tests" ]; then 39 | tests="$filename" 40 | else 41 | tests="$tests,$filename" 42 | fi 43 | done 44 | echo "--grep input to send: $tests" 45 | echo "test_files=$tests" >> "$GITHUB_OUTPUT" 46 | 47 | trigger-tests-on-a-pull-request: 48 | needs: touched-test-files 49 | uses: ./.github/workflows/reusable-workflow-to-run-tests.yml 50 | with: 51 | tags-of-tests-to-include: "unit" 52 | test-files-to-include: "${{needs.touched-test-files.outputs.test_files_to_run}}" 53 | tags-of-tests-to-exclude: "" 54 | run-name: "${{ github.event.pull_request.title }}" 55 | secrets: inherit 56 | -------------------------------------------------------------------------------- /.github/workflows/trigger-smoke-tests-on-merge-to-main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: trigger-tests-on-merge-to-main 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | 10 | jobs: 11 | trigger-tests-on-merge-to-main: 12 | uses: ./.github/workflows/reusable-workflow-to-run-tests.yml 13 | with: 14 | tags-of-tests-to-include: "smoke" 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/trigger-specific-targetted-tests-on-an-external-event.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: trigger-tests-on-an-external-event 5 | 6 | on: 7 | repository_dispatch: 8 | types: [deployment-completed-notification-event] 9 | 10 | jobs: 11 | trigger-tests-on-an-external-event: 12 | uses: ./.github/workflows/reusable-workflow-to-run-tests.yml 13 | with: 14 | tags-of-tests-to-include: ${{ github.event.client_payload.TAG }} # -Dgroups="" means execute all test cases 15 | tags-of-tests-to-exclude: "" # -DexcludedGroups="" means don't exclude any test case 16 | test-environment: ${{ github.event.client_payload.TEST_ENV }} 17 | secrets: inherit 18 | -------------------------------------------------------------------------------- /.github/workflows/trigger-tests-manually.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: trigger-tests-manually 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | tags-of-tests-to-include: 10 | description: 'Tag of tests to include' 11 | required: false 12 | default: '' 13 | type: choice 14 | options: 15 | - '' # -Dgroups="" means execute all test cases 16 | - 'smoke' 17 | - 'flaky' 18 | - 'failing' 19 | - 'flaky, failing' 20 | 21 | tags-of-tests-to-exclude: 22 | description: 'Tag of tests to exclude' 23 | required: false 24 | default: '' 25 | type: choice 26 | options: 27 | - '' # -DexcludedGroups="" means don't exclude any test case 28 | - 'smoke' 29 | - 'flaky' 30 | - 'failing' 31 | - 'flaky, failing' 32 | 33 | test-environment: 34 | description: 'Test environment' 35 | required: false 36 | default: 'STAGING' 37 | type: choice 38 | options: 39 | - 'DEVELOP' 40 | - 'STAGING' 41 | 42 | jobs: 43 | trigger-tests-manually: 44 | uses: ./.github/workflows/reusable-workflow-to-run-tests.yml 45 | with: 46 | tags-of-tests-to-include: ${{ inputs.tags-of-tests-to-include }} 47 | tags-of-tests-to-exclude: ${{ inputs.tags-of-tests-to-exclude }} 48 | test-environment: ${{ inputs.test-environment }} 49 | secrets: inherit 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all .idea files 2 | .idea 3 | 4 | # Ignore all uninterested files 5 | notes.txt 6 | 7 | # Ignore target 8 | target 9 | 10 | # Ignore git crypt key (in a real production world scenario). 11 | # I am not ingoring it here since its an open source project and anyone who wants to clone the project would need this 12 | # key to work with. Ideally, if you were working in a company, this key would be preserved in a password manager such 13 | # as 1password from where everyone could download and decrypt files. 14 | # git-crypt-key-restpro 15 | 16 | # Ignore mac file 17 | .DS_Store 18 | 19 | # Ignore .cache files 20 | .cache 21 | 22 | # Ignore secrets file 23 | **/http-client.private.env.json 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-json 7 | - id: check-xml 8 | - id: mixed-line-ending 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | 12 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 13 | rev: 54f7f7c5cb9501110b928f2a98faa8024142f8d7 14 | hooks: 15 | - id: pretty-format-java 16 | args: [--autofix] 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Appium", 4 | "dockerignore", 5 | "dockerized", 6 | "powertester", 7 | "Qube" 8 | ], 9 | "java.configuration.updateBuildConfiguration": "automatic", 10 | "java.compile.nullAnalysis.mode": "automatic" 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | pramodyadav027@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🦾 restpro 2 | 3 | Test RestAPIS like a PRO. 4 | 5 | ![build status](https://img.shields.io/github/actions/workflow/status/pramodkumaryadav/restpro/trigger-tests-on-pull-request.yml?logo=GitHub) 6 | ![open issues](https://img.shields.io/github/issues/PramodKumarYadav/restpro) 7 | ![forks](https://img.shields.io/github/forks/PramodKumarYadav/restpro) 8 | ![stars](https://img.shields.io/github/stars/PramodKumarYadav/restpro) 9 | ![license](https://img.shields.io/github/license/PramodKumarYadav/restpro?style=flat-square) 10 | ![languages](https://img.shields.io/github/languages/count/pramodkumaryadav/restpro) 11 | ![info](https://img.shields.io/static/v1?label=with-love-from&message=power-tester&color=blue?style=plastic&logo=appveyor) 12 | 13 | > NOTE: 14 | > 1. I found that a few users who tried to use this project struggled to use git-crypt properly to decrypt encrypted secrets and thus were not able to move forward with rest of learnings too. 15 | > 2. To make the entry barrier minimum for new users, I am removing encryption from [secrets.conf](src/main/resources/develop/secrets.conf) files and storing them as plain text for training purpose. 16 | > 3. That said, in a real project you should use the instructions mentioned here in this readme file [**git-crypt**](docs/README-GIT-CRYPT.md) to encrypt/decrypt secrets and MUST not store your secrets in plain text. 17 | 18 | ## Application under test 19 | 20 | [Restful booker](https://restful-booker.herokuapp.com/) - An API playground created by [Mark Winteringham](https://www.mwtestconsultancy.co.uk/) for those wanting to learn more about API testing and tools. 21 | 22 | ## 🔢 Requiring (one time) manual setup by user 23 | 24 | 1. [**JDK 11**](https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html) - as language of choice 25 | for writing this test framework. 26 | 2. [**Maven 3.8.6+**](https://maven.apache.org/) - for project dependency management and running tests in CI. 27 | 3. [**pre-commit**](docs/README-CODE-FORMATTING.md) - To have code automatically and uniformly formatted (JAVA, JSON, 28 | XML, YAML). 29 | 30 | ## 🚀 Core features 31 | 32 | - [x] Allow to put both exploratory tests (postman style http requests) and automated tests (RestAssured) side-by-side 33 | in the same framework. Usually due to Postman not being git compatible, these two tests live in two separate 34 | places/repositories. 35 | - [x] Shows how to create a decoupled test design (that minimises maintenance efforts by reducing code 36 | duplication and increases code readability by separating test intentions from implementation details). 37 | - [x] Allows users to write fluent assertions for asserting both status and response body, without any code duplication. 38 | - [x] Creates APIs that are scope/role agnostic. This allows you to use the same APIs and data to write tests for 39 | different user roles and scopes. 40 | - [x] Shows how to provide different token types for different role/scopes using a TokenFactory pattern. 41 | - [x] Shows how to reuse the same auth token in all the tests for same role/scope using a Singleton pattern. 42 | - [ ] Shows how to use health checks in the test CI to have efficient pipelines. 43 | - [ ] Shows how to insert test data dynamically in each test. 44 | 45 | > A Note on Postman style (HTTP Request) tests from AQUA: 46 | > - These tests are git compatible (unlike Postman that requires you to take a paid 47 | membership for properly version controlling its collections). In these HTTP request tests: 48 | > - User can define different environments (such as localhost, develop, staging). For each environment, user can 49 | specify env specific configuration such as hostURLs. 50 | > - Provides a way to separate secret information (such as userid/password) from other generic config information. 51 | > - Provides a way to run pre-request and post-request scripts that can be used to set variables such as auth-tokens. 52 | > - Until AQUA IDE is closedThese auth-tokens/variables are then available to any other http request that needs to use 53 | these tokens/variables. 54 | > - Provides a way to run post-request scripts that can be used to set variables, log values and write some basic tests. 55 | 56 | ## 🎯 Standard features 57 | 58 | > From RestAssured testing and [Core test framework: zero](https://github.com/PramodKumarYadav/zero) are as below: 59 | 60 | - [ ] Shows how to integrate your tests in CI (GitHub Actions). 61 | - [ ] Shows how to log your test results into a test monitoring system (such as Elastic/Kibana or DataDog) 62 | - [x] Shows how to do JSON Schema validation. 63 | - [x] Shows how to separate config from code. 64 | - [x] Shows how to deal with Secrets in local and in CI. Also on how to skip logging secret information on console. 65 | - [ ] More to be added... 66 | 67 | ## ⚙ Tool Set 68 | 69 | Key tools to be used in this core framework are: 70 | 71 | - [x] **Java** (As the core programming language) 72 | - [x] **Maven** (for automatic dependency management) 73 | - [x] **Junit 5** (for assertions) 74 | - [x] **RestAssured** (library for Rest API automation) 75 | - [x] **Slf4J/Log4J** (for logging interface and as a logging library) 76 | - [x] **Typesafe** (for application configuration for multiple test environments) 77 | - [x] **Git crypt** (for managing secrets) 78 | - [x] **Surefire** (for xml reports in CI) 79 | - [x] **Surefire Site plugin** (for html reports in CI) 80 | - [x] **GitHub** (for version control) 81 | - [x] **GitHub actions** (for continuous integration) 82 | - [x] **Faker library** (for generating random test data for different locales - germany, france, netherlands, english) 83 | - [x] **Slack integration** (for giving notifications on pull requests) 84 | - [x] **Elastic and Kibana** (for test monitoring) 85 | - [ ] **Docker** (for automating test framework's environment) 86 | - [ ] **Powershell or bash Script** (for automating building test environment) 87 | - [ ] **SonarQube/SonarLint** (for keeping your code clean and safe) 88 | - [x] **Badges** (for a quick view on your project meta and build status) 89 | 90 | ## 🧪 api-test-design 91 | 92 | ![api-test-design](./images/api-test-framework-design.png) 93 | 94 | ## 🔚 end-to-end-test-workflow 95 | 96 | ![end-to-end-test-workflow](./images/end-to-end-test-workflow.png) 97 | 98 | ## ℹ References 99 | 100 | - Rest-assured 101 | - [Application under test (restful-booker)](https://restful-booker.herokuapp.com/apidoc/index.html) 102 | - [Info on HTTP Client](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) 103 | - [Exploring-http-syntax](https://www.jetbrains.com/help/idea/exploring-http-syntax.html) 104 | - [http-response-handling-api-reference](https://www.jetbrains.com/help/idea/http-response-handling-api-reference.html) 105 | 106 | ## 🔌 Plugins 107 | 108 | - [HOCON](https://plugins.jetbrains.com/plugin/10481-hocon) - For config and secrets files syntax highlight. 109 | - [.ignore](https://plugins.jetbrains.com/plugin/7495--ignore) - For (dockerignore, gitignore) files syntax highlight. 110 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | services: 3 | es01: 4 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.8 5 | container_name: restpro-es01 6 | environment: 7 | - node.name=es01 8 | - cluster.name=es-docker-cluster 9 | - discovery.seed_hosts=es02,es03 10 | - cluster.initial_master_nodes=es01,es02,es03 11 | - bootstrap.memory_lock=true 12 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 13 | ulimits: 14 | memlock: 15 | soft: -1 16 | hard: -1 17 | volumes: 18 | - data01:/usr/share/elasticsearch/data 19 | ports: 20 | - 9200:9200 21 | networks: 22 | - elastic 23 | es02: 24 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.8 25 | container_name: restpro-es02 26 | environment: 27 | - node.name=es02 28 | - cluster.name=es-docker-cluster 29 | - discovery.seed_hosts=es01,es03 30 | - cluster.initial_master_nodes=es01,es02,es03 31 | - bootstrap.memory_lock=true 32 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 33 | ulimits: 34 | memlock: 35 | soft: -1 36 | hard: -1 37 | volumes: 38 | - data02:/usr/share/elasticsearch/data 39 | networks: 40 | - elastic 41 | es03: 42 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.8 43 | container_name: restpro-es03 44 | environment: 45 | - node.name=es03 46 | - cluster.name=es-docker-cluster 47 | - discovery.seed_hosts=es01,es02 48 | - cluster.initial_master_nodes=es01,es02,es03 49 | - bootstrap.memory_lock=true 50 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 51 | ulimits: 52 | memlock: 53 | soft: -1 54 | hard: -1 55 | volumes: 56 | - data03:/usr/share/elasticsearch/data 57 | networks: 58 | - elastic 59 | 60 | kib01: 61 | image: docker.elastic.co/kibana/kibana:7.17.8 62 | container_name: restpro-kib01 63 | ports: 64 | - 5601:5601 65 | environment: 66 | ELASTICSEARCH_URL: http://es01:9200 67 | ELASTICSEARCH_HOSTS: '["http://es01:9200","http://es02:9200","http://es03:9200"]' 68 | networks: 69 | - elastic 70 | 71 | volumes: 72 | data01: 73 | driver: local 74 | data02: 75 | driver: local 76 | data03: 77 | driver: local 78 | 79 | networks: 80 | elastic: 81 | driver: bridge 82 | -------------------------------------------------------------------------------- /docs/README-CODE-FORMATTING.md: -------------------------------------------------------------------------------- 1 | # Code formatting Setup (one time) 2 | 3 | Reference: https://pre-commit.com/ 4 | 5 | Install pre-commit to have code automatically and uniformly formatted (JAVA, JSON, XML, YAML). 6 | 7 | ## Short version 8 | 9 | To install pre-commit do below steps (as a one time activity). 10 | - Open terminal 11 | - Install [pre-commit](https://pre-commit.com/) (a hooks package manager). 12 | - If on mac, install using: `brew install pre-commit` 13 | - If on windows, install using pip (python package manager). 14 | - Install python and pip first if not intalled already. 15 | - Then run `pip install pre-commit` 16 | - Check pre-commit version by running: `pre-commit --version` 17 | - cd to your project repository. 18 | - Run `pre-commit install` 19 | - That's it! From now on if you try to push any unformatted code to GitHub, pre-commit hook will both format the code 20 | and show the changed file for you to stage and commit. 21 | 22 | ## Long version 23 | 24 | ### Step 1 25 | 26 | - Install [pre-commit](https://pre-commit.com/) which is a package manager for installing pre-commit hooks. 27 | 28 | ### FYI Step: (fyi only - already done) 29 | 30 | > NOTE: This particular step is already done and is only here for your information. No action is required from new users, 31 | since Hook file is already copied in the project. I am adding this step here since it is good to know how this was done and so that if needed in future you can add (new) hooks here. 32 | 33 | - Search for the pre commit hook you are interested in from the list of [available hooks](https://pre-commit.com/hooks.html). 34 | - If you search with word "java" in the search window of above webpage, you will find (at least) 3 different pre commit hooks. 35 | - All of these hooks use [Google Java Format program](https://github.com/google/google-java-format) that reformats Java source code to comply with [Google's Java codestyle](https://google.github.io/styleguide/javaguide.html). 36 | - The hook we have chosen for our project is named `google-style-java` from user [matltz - on Github](https://github.com/maltzj/google-style-precommit-hook). 37 | - Click on the above GitHub URL to go to see how to use this hook. You will see an example as below 38 | 39 | ``` repos: 40 | - repo: https://github.com/maltzj/google-style-precommit-hook 41 | sha: b7e9e7fcba4a5aea463e72fe9964c14877bd8130 42 | hooks: 43 | - id: google-style-java 44 | ``` 45 | 46 | - Add a file in root of this repository named `.pre-commit-config.yaml` and paste above content in it (already done. Listed for fyi only). 47 | - Change tag `sha` to `rev` since that is how it used in latest versions (already done). 48 | - Add any other hooks you are interested in here (as you will see in file `.pre-commit-config.yaml`). 49 | 50 | #### Step 2 51 | 52 | - Each user need to run below step (Only one time ever for a project repo): 53 | - To install the git hook script on your local git repository: 54 | - run `pre-commit install` from the root of this repository. 55 | - Should give result as below if successful. 56 | 57 | ```bash 58 | $ pre-commit install 59 | pre-commit installed at .git/hooks/pre-commit 60 | ``` 61 | 62 | #### Step 3 63 | 64 | - That's it you are now all set! 65 | - Pre-commit will now run on every commit that you make and your code will be auto-magically formatted as per [Google's Java codestyle](https://google.github.io/styleguide/javaguide.html) 66 | - If there are any formatting violations, which they will be almost "All-of-the-time" (as below). Not only it will tell you what the failures are 67 | but it will also fix it for you (of course you would need to add those changes to stage and commit) 68 | 69 | ```bash 70 | 2023-02-25 20:40:19.553 [info] Google Java Code Style for Java......................(no files to check)Skipped 71 | Trim Trailing Whitespace.................................................Failed 72 | hook id: trailing-whitespace 73 | exit code: 1 74 | files were modified by this hook 75 | 76 | Fixing README.md 77 | ```bash 78 | - You can see failures here: 79 | - `Trim Trailing Whitespace.................................................Failed` 80 | - Here it tells you that the hook is fixing these issues for you: 81 | - ``` 82 | files were modified by this hook 83 | 84 | Fixing README.md 85 | ``` 86 | - Now all you need to do is stage and add those changes and that's it! 87 | - Once you stage and commit, you should see now a message as below where everything should pass. 88 | - ``` 89 | 2023-02-25 20:42:57.360 [info] Google Java Code Style for Java......................(no files to check)Skipped 90 | Trim Trailing Whitespace.................................................Passed 91 | Check Yaml...........................................(no files to check)Skipped 92 | Check JSON...........................................(no files to check)Skipped 93 | Pretty format JSON...................................(no files to check)Skipped 94 | Check Xml............................................(no files to check)Skipped 95 | Fix End of Files.........................................................Passed 96 | Don't commit to branch...................................................Passed 97 | ``` 98 | 99 | #### Step 4 [One time action for the whole project by any user] 100 | 101 | When pre-commit is first introduced in a project it is advised to format all the files at least 102 | one time to avoid seeing formatting changes going forward. The way you can do this is by running 103 | the below command in a powershell or a git terminal 104 | 105 | > Note: It would not work from intellij or normal terminals. 106 | 107 | `java -jar "C:\Users\Pramod Yadav\.cache\pre-commit\google-java-formatter1.16.0.jar" --replace $(git ls-files *.java)` 108 | 109 | > Here you to replace the "Pramod Yadav" user with your user directory where this jar file was downloaded. 110 | > Once formatted push all the changes in a PR and you can be assured that you dont have to see any formatting related 111 | > issues anymore in your project PRs. 112 | > 113 | [Back to readme.md](./readme.md) 114 | -------------------------------------------------------------------------------- /docs/README-GIT-CRYPT.md: -------------------------------------------------------------------------------- 1 | # Setting up git-crypt to encrypt your secrets 2 | 3 | Our secret keys are saved in `secrets.*` files and encrypted with [`git-crypt`](https://github.com/AGWA/git-crypt#readme). 4 | 5 | > A user who is not added to the project, will not be able to use the secrets from our project and thus this is a mandatory 6 | > step to complete to be able to run the tests. 7 | 8 | ## Install Git-Crypt 9 | 10 | Reference: https://dev.to/heroku/how-to-manage-your-secrets-with-git-crypt-56ih 11 | 12 | ### Step1: Verify if you have git crypt installed on your system 13 | 14 | - macOS/Linux (run from terminal): `git-crypt --version` 15 | - Windows (run from gitbash or powershell): `git-crypt --version` 16 | 17 | ### Step2: Install git crypt, if you haven`t already 18 | 19 | > If you already see git-crypt installed in the previous step, skip. 20 | 21 | Install `git-crypt` on your system: 22 | 23 | - macOS (with homebrew) `brew install git-crypt` 24 | - Windows - [Download git-crypt.exe and place it here: C:\Program Files\Git\cmd\git-crypt.exe](https://github.com/oholovko/git-crypt-windows). 25 | - Linux `sudo apt install git-crypt` 26 | - [Manual installation](https://github.com/AGWA/git-crypt/blob/master/INSTALL.md) 27 | 28 | ## Encrypt Project 29 | 30 | > One time activity, to be done by the very first user of this project. 31 | 32 | > Note if the project is already git crypt-ed by another user, skip this section and go to the next section. 33 | 34 | Below are the steps that needs to be done only one time for the project by the very first user, who tries to 35 | set up git -crypt in the project repository. Run below commands to git crypt the project. 36 | 37 | > Install git crypt if not already installed. 38 | 39 | 1. `cd repo` 40 | 2. `git-crypt init` 41 | 3. `git-crypt export-key ./git-crypt-key-restpro` 42 | - Save this in a central password manager - like `1password`. 43 | 4. define which files to encrypt in `.gitattributes` files. 44 | - Ex: `secrets.conf filter=git-crypt diff=git-crypt` 45 | 5. Check before committing. 46 | `git-crypt status` 47 | 6. Ignore the key `git-crypt-key-restpro` from version control by adding it to the `.gitignore` file. 48 | > Ignore git crypt key (in a real production world scenario). 49 | > I am not ignoring it here since its an open source project and anyone who wants to clone the project would need this 50 | > key to work with. 51 | 52 | > Ideally, if you were working in a company, this key would be preserved in a password manager such 53 | > as 1password from where everyone could download this key and decrypt files. 54 | 55 | 7. Push files to github 56 | 8. Check if files are encrypted on github by clicking on any secrets file in Github and by verifying that 57 | text is not readable. 58 | 59 | ## Decrypt Project (in local) 60 | 61 | > One time activity, to be done by every new user of this project. 62 | 63 | Now once a user has initialized a project with git crypt 64 | 65 | 1. other new users can simply ask for the key from the first user 66 | or download it from a central password manager tool (recommended) - such as `1password` or any other password manager 67 | tool. 68 | 2. They have to copy/paste this file in their cloned projects root directory. 69 | 3. Then run (only one time) below command to see the decrypted files. 70 | - `git-crypt unlock git-crypt-key-restpro` 71 | 72 | ## Decrypt Project (in CI) 73 | 74 | Refer information [here](https://github.com/sliteteam/github-action-git-crypt-unlock), to see how this was done. 75 | 76 | > NOTE: If you are making a copy of this project and pushing it to your own GitHub repository, 77 | > remember to run the below command from say (gitbash terminal) 78 | 79 | > `git-crypt export-key ./git-crypt-key-restpro && cat ./git-crypt-key-restpro | base64` 80 | 81 | > to get the secret and add it to GitHub secret named: GIT_CRYPT_KEY. 82 | > Since this is a demo project, the key is already present in the root repository and I do not 83 | > mind exposing this secret for you below. In a real life project, this will not be part of version 84 | > control. 85 | 86 | ```commandline 87 | Pramod Yadav@DESKTOP-GPU5LFR MINGW64 ~/restpro (main) 88 | $ git-crypt export-key ./git-crypt-key-restpro && cat ./git-crypt-key-restpro | base64 89 | AEdJVENSWVBUS0VZAAAAAgAAAAAAAAABAAAABAAAAAAAAAADAAAAIODz1YHHA96CZubMzshhXpKh 90 | SIuNpeEPbQmvIcBT8UTuAAAABQAAAEAfT0bYmgWbxK+RI/mKsJXtCq9Th77lSR0D1G/5WGfspccv 91 | o/0VHDfAHi88Q6LmCL45TixGqFnLi5XmqzFwBjgdAAAAAA== 92 | 93 | ``` 94 | 95 | ## Reference 96 | 97 | - [git-crypt official GitHub repo and readme file](https://github.com/AGWA/git-crypt) 98 | - [A great article on this topic by Michael Bogan for Heroku](https://dev.to/heroku/how-to-manage-your-secrets-with-git-crypt-56ih) 99 | -------------------------------------------------------------------------------- /drawings/api-test-framework-design.drawio: -------------------------------------------------------------------------------- 1 | 7V1bc5u6Fv41nmkfkuFu8+hb2uSkO2mSTs/uyx4ZZJsGIxfwJfvXbwkkA5KwnRjZaZO204CQhND61kVrSSstsz9bf4rBfPoF+TBsGZq/bpmDlmHotqnhH6TkKS9x2kZeMIkDn1YqCu6DfyEtpO0mi8CHSaViilCYBvNqoYeiCHpppQzEMVpVq41RWH3rHEygUHDvgVAs/R746TQv7dhaUf4ZBpMpe7Ou0SczwCrTgmQKfLQqFZnDltmPEUrzq9m6D0MyeWxe8nYXNU83A4thlO7T4LI/GX2fXlv+P/N/7U4bmqPg6oz2sgThgn5w9/YSF8DIn6MAd5wPPX1i84G/Yk4u5zHyYIInt7eaBim8nwOPFK8wCnDZNJ2F+E7HlyPgPU5itIj8m0UaBhGk5eMgDPsoRHHWrzmwhvqFjcvFz2JjhHEK16Ui+pmfIJrBNH7CVehTR7fzJhRzhuXk96uCgjojy7REPYeWAQqayabrYl7xBZ3aZ0yzIUxzy3DClEwDIpNcmmHn1wKxB2dJxg9dXEG35uviIb6akJ+3N/cPrKdRvCn9Jin8NNwU4k/IX5s/EWiMid8lrIPvvBAkSeBVSZpRE5Lv1fAd9DHD0MYoTqdogiIQDovSHqZk/PR/Vp3c/E1uzm12O1iXHw6e6F0tFBK0iD24G9YpiCcw3VKP4oR8wlZgxTAEabCsygQZSmjT25x3GCBtJhQoIC0GSNZF/kG3G467qOmo3al2ZHS4jvIvFjrKQLv5npfj2BZwfHV/8xcuuYPJHEUJFLBUYEXfLSs4mXCB//T7zciETts6d53q5JmnFgttiVhQzYvrICWsqJ9repve59zo0ruCGcnNU+nmFsYB/nAYl7n1VfI1sx5UM7bjVvnRdBpibKGjGsbG2ABPpWqZ0k7qB9x2uPdo2vZx8fVth0N9PoJGpUxHkbYcDK+HD8O9tSDGzjUYYXsWcxnEfYNR9kirMiAIg0lEuBODmXBGj8ikABuQXfpgFvh+xnoi7LcJBWrQ0pcWZuQeIu9AUBvtags0HidQiTphbFqitEEAWa9OcKfY8oe7VQlI5vlyYBysiXjkdYvT7vbci4Z0iyvoFlMXdUtHolo6qlQLe39pavGcwGxufy1gQugtGon3eNKCMUZvGqDoeLqooofsrXroAN3ROY5OsDidgGXoy3SCo3MdKdIJVpt7j75dJ9jGgfWdI+gQXbStNqYqw38P+U8CyF+LwWra3Ky5p7ZWdVEvf4bAhzHGlgNmZGqiUTLPFS4nWM5wuy8wBT5IwX61uwssMmKsdXNRpH1IPDSHH+WViWj7dncpf9gH3jSIJjUPEXoMYELkQuo1CoahQ/42BAazCgadEbkMBksCho07qnk0uIqstNyngQellfwYR3dZHFc1tY+jmng/hGlZ53ZDysnlgNaQcrI55JuGs3VcfH02LqXKxhDdqMoRWos25fYNJ4pMbrmwL4J0a1dPip1ZhvmGqMbZYJbdFNWEnlRTTfRBKqcaC3qEQfR4SiKanNDt8Np9XyryHdn7Ce/GaFjv4ZnqTMfngaiH3FS/iMEMrlD8iK8HMCGelRorEg+o6IMV+sFys8SdkpggtpIwPPAHZ4PFdAzS3ApkcUQSZsRTMIVxdtkiAb1HsnwGxOrCA8ksSi0fZJoPcswGeV4aTOnVAk6x9ZdWkZmkMXqEzIqMEAmXVQxLWpRgK5SMwBzYxd0DwhMxODO0GlsVYYNzHGZsMQ18H0YCK9Qie39TlRfqbXHZYkgtVd6AaC74Vm+pFkihOBtukPAMdM1ZwRVYEnj0iczJ0JROQRVK8l4x/SLWRzCbh3AGM6MItyeQ8yngcxyOC1bYjKncQal4/qaQZ7rnzILYiLW2DHznjinBHzMHGsefeQLr8LVoLLvdkMbiOzqyxjJPEKp7LTTs2A3RkO/o2DQUrY7hOvdE9IgXKJrk2jwT3wJ1k1UwC0EuFVGUMtIRwnrTIPSvwRNaEMIkKfAe2V0v82Hh+qCQsyBO6T4nU6vUuCctKTyyiBO8ZdTWuaIvYF2peA2SlBZ4KAzBPAlGm/HN8LwGUQ+lKZrRSgp339guT2WpED6mBWCKFsAdTBdxlOTkrw03UTW5UzeWp5AFBEM4Jm1rw4FUdV5n1QZWUXJHp8SqUZzUZYLfavfwPzxpfeKasgfEa2b39OIe/yPV47SPvy6NQZARFWKsrIi5Q0RSCtJSXLOKbAEL29lqN0I2Qef9AGGqwgN7WQkPmK1AvrfB0HSCir7R6pIIWStbLGHOoAUavaj5XwiybQ+pvTWAKcOUxCEuxRSvc5rDlGjllTFl5JJmC6A2WigPS2kfOEd4dtF96H/+KHYi/P91AeOn7P1kGfHh0/BB0kp89aWfvbjyuvwy2z5ILvJNFB/fkawKybq+J5SV7R2wRMcoZygVCvMtWkuN7kvs4FWr4xZ/dttOusx2MpSBwdrXdiLvNrSrJAvXotHPzI32cimxmeXfWUq8xKDasN9BBpUMJeoMKtEp/414wKQIub25unnzwFCGBZkhdFwsiDvPCixcYZmdCwiyG+cy8sKFf9Cy6x0M28DgnhwMot+MMyXepgnR6OYgPiLb0UWqa8c1GkRXG2c0ZMvjxtbGm+n8naXAi2yF9t5o2WIryMChTiSIzjjB+ZLt6As8/HOzw7BfXT2Psh1/128eNMpwIrMjjooT5qmR4SQpPCrAI+dQHzC13yWIMjDI7IjjgqE+dtNNEvwdb9qawCbwsKnDLLreEeLoliu6ITbRmuNYFPaOEM4S4eYHrSPoFP7O/P8SC8JuInwjBYMyYeBs0QytcvhmuCaHwKBfcUIIXvCLFud6f+MoUgYc2aGF4wJnrxhN10sXIGw1ExZ+R8Qz7YojI+IEe8C5fT2ZRcFeQEFUq8fLR0muQHu8RObP3v1XYFt/+VfzB59lGlG9J0jXuXMWTueFB0mEnvbcFfTcgyTCe3Ydfd/VQM1REkcMt+QG7jZ59MecUtA17oSSYb/w8Kxu8EmADI0/69QYshz5oOuRxTegM6UWWWKMBmTI+gALM2mZZEtqqgBjCjgx2h9P0Wy02CP9k8qViqG3z6tE1m3JIVnTFhWKrk6jiOEPzuNN93e8yXVqg+ejOa+3o5/c6+2IwY7KGpUS/j1M3sjCdcNov4/r2xG9WEVotIyO9xD5/quO5+Pg5K5tR3RmvYfITwSGk7u22cGWrcfTmkmtcD34kj++RhNC+VZxmFKjqVtr0i1IT7/JIfmcM2UZOodOhs7Xe7CMszQ2udl2bdC0lEFGdqJMDWRuGWRup0/JO2YOOIzoyI89SA8jNoGc0f++2r8uF1bP/Ntfjpa6t15FkkS0gHkXapaBH0vrQHzpSXRRY0nZml0dmtW1oalJ1obSBUITOXOksy86GyW+Hc1Dsjl+7Tl5yzk5t0FPtQvJbXNkd8zz9ot9k3x+T6NzbqjxIgnjbjjTppQmezkbjw3Ic82qYtI0d4BSnq2pvT1dU9YFn9K2Ft47czjZr4wNOhyc2gewgWHxPKUmE6Gu8UnvOvr2kfENmJDf2/X6/Ab0MGlTfDl56P2YfH+86f/4avfS2dI8+7WQpOW/HAgceEhyuoE97GTLuCZMHZ4GDHrHyFQonT7RymnIGGYH47jTcH9clrptoDx6sny9sSR1puYqkVt8BlXT3S5UhHG5zep6aWS4fs04anbJeI91KghJJebIrGa9ybLY5MlYd60cR40vHDlX2O6s3TK52ryHgU//fsw9Vtv2ETQuQgXMDWBSwguFhYgXCiQJMPYTvX8WXlyXM8hcSfBLVUoF0/x+Z93frH4u7n/crIK+lgz7UulSr/3obB8hOU7ZHn9VG2ZenEXnZBtmrGoq1Wc3OFitQfufi7uHOTwD0/HsyjEe0q4ceGpcoTRF3CBPYX18n2e/77qqfZ6CCJIwVf1CQIC4IJRkvwOhCWenFBr1OkwRNIbRskVSikfjYHISiLiuerf4QRCxDHsXRKRLRWUYke3/VIqRO5gswvQU6Oj0h68cHZIdXRKAyOxgZfgQPaT1+MizcB6wbFoGHjGCaTpZTDhDu8T25iRmvwSBPvmwCvBkGNoMxVn9afZeQnWWkZb9/saPJUTVJAltHGhdt28Ne68aaI6xU1NZTjMww7fF7/jMjZ7iN6Waw/8A 2 | -------------------------------------------------------------------------------- /git-crypt-key-restpro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PramodKumarYadav/restpro/717ba2126d92abe23c0c908667c31c0aa9514098/git-crypt-key-restpro -------------------------------------------------------------------------------- /images/api-test-framework-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PramodKumarYadav/restpro/717ba2126d92abe23c0c908667c31c0aa9514098/images/api-test-framework-design.png -------------------------------------------------------------------------------- /images/end-to-end-test-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PramodKumarYadav/restpro/717ba2126d92abe23c0c908667c31c0aa9514098/images/end-to-end-test-workflow.png -------------------------------------------------------------------------------- /images/github-pr-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PramodKumarYadav/restpro/717ba2126d92abe23c0c908667c31c0aa9514098/images/github-pr-workflow.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.powertester 8 | restpro 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 17 13 | ${java.version} 14 | ${java.version} 15 | 3.10.1 16 | 3.0.0-M5 17 | 3.11.0 18 | 5.10.1 19 | 1.18.30 20 | 1.4.2 21 | 8.6.1 22 | 2.14.1 23 | 2.0.1 24 | 5.3.2 25 | 3.24.2 26 | 5.1.0 27 | 4.2.0 28 | 2.0.9 29 | 1.4.13 30 | 2.0.2 31 | 32 | 33 | 34 | 35 | io.rest-assured 36 | rest-assured 37 | ${rest-assured.version} 38 | 39 | 40 | io.rest-assured 41 | json-schema-validator 42 | ${rest-assured.version} 43 | test 44 | 45 | 46 | 47 | org.junit.jupiter 48 | junit-jupiter 49 | ${junit.jupiter.version} 50 | test 51 | 52 | 53 | 54 | org.assertj 55 | assertj-core 56 | ${assertj-core.version} 57 | test 58 | 59 | 60 | 61 | org.projectlombok 62 | lombok 63 | ${lombok.version} 64 | 65 | 66 | org.junit.platform 67 | junit-platform-console-standalone 68 | 1.9.0-M1 69 | 70 | 71 | 72 | com.typesafe 73 | config 74 | ${typesafe.version} 75 | 76 | 77 | co.elastic.clients 78 | elasticsearch-java 79 | ${elasticsearch-java.version} 80 | 81 | 82 | 83 | com.fasterxml.jackson.core 84 | jackson-databind 85 | ${jackson-databind.version} 86 | 87 | 88 | jakarta.json 89 | jakarta.json-api 90 | ${jakarta.json-api.version} 91 | 92 | 93 | 94 | net.datafaker 95 | datafaker 96 | ${datafaker.version} 97 | 98 | 99 | 100 | com.zaxxer 101 | HikariCP 102 | ${HikariCP.version} 103 | 104 | 105 | 106 | org.awaitility 107 | awaitility 108 | ${awaitility.version} 109 | 110 | 111 | 112 | org.slf4j 113 | slf4j-api 114 | ${slf4j.version} 115 | 116 | 117 | 118 | ch.qos.logback 119 | logback-classic 120 | ${logback-classic.version} 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-compiler-plugin 129 | ${maven.compiler.plugin} 130 | 131 | UTF-8 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-surefire-plugin 138 | ${maven.surefire.plugin} 139 | 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-site-plugin 144 | ${maven.site.plugin} 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-jar-plugin 149 | 3.2.2 150 | 151 | 152 | 153 | test-jar 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-surefire-report-plugin 166 | ${maven.surefire.plugin} 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /restpro.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/annotations/CsvTest.java: -------------------------------------------------------------------------------- 1 | package org.powertester.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | 9 | @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @ParameterizedTest(name = "[{index}] {0}") 12 | public @interface CsvTest {} 13 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/annotations/FailingTest.java: -------------------------------------------------------------------------------- 1 | package org.powertester.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.api.Tag; 8 | 9 | @Target({ElementType.METHOD, ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Tag("failing") 12 | public @interface FailingTest {} 13 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/annotations/FlakyTest.java: -------------------------------------------------------------------------------- 1 | package org.powertester.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.api.Tag; 8 | 9 | @Target({ElementType.METHOD, ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Tag("flaky") 12 | public @interface FlakyTest {} 13 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/annotations/HealthCheckTest.java: -------------------------------------------------------------------------------- 1 | package org.powertester.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.api.Tag; 8 | 9 | @Target({ElementType.METHOD, ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Tag("healthcheck") 12 | public @interface HealthCheckTest {} 13 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/annotations/RegressionTest.java: -------------------------------------------------------------------------------- 1 | package org.powertester.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.api.Tag; 8 | 9 | @Target({ElementType.METHOD, ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Tag("regression") 12 | public @interface RegressionTest {} 13 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/annotations/SmokeTest.java: -------------------------------------------------------------------------------- 1 | package org.powertester.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.api.Tag; 8 | 9 | @Target({ElementType.METHOD, ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Tag("smoke") 12 | public @interface SmokeTest {} 13 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/annotations/UnitTest.java: -------------------------------------------------------------------------------- 1 | package org.powertester.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.api.Tag; 8 | 9 | @Target({ElementType.METHOD, ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Tag("unit") 12 | public @interface UnitTest {} 13 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/auth/AuthBody.java: -------------------------------------------------------------------------------- 1 | package org.powertester.auth; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.experimental.Accessors; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | @JsonInclude(JsonInclude.Include.NON_NULL) 13 | @Data 14 | @Builder(setterPrefix = "set") 15 | @Accessors(chain = true) 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class AuthBody { 19 | private String username; 20 | private String password; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/auth/Scope.java: -------------------------------------------------------------------------------- 1 | package org.powertester.auth; 2 | 3 | public enum Scope { 4 | GUEST("read"), 5 | MAINTAINER("write"), 6 | ADMIN("delete"); 7 | 8 | private String value; 9 | 10 | Scope(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String getValue() { 15 | return value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/auth/TokenFactory.java: -------------------------------------------------------------------------------- 1 | package org.powertester.auth; 2 | 3 | import static io.restassured.RestAssured.given; 4 | 5 | import com.typesafe.config.Config; 6 | import io.restassured.response.Response; 7 | import java.util.Arrays; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.powertester.config.TestConfig; 10 | 11 | @Slf4j 12 | public class TokenFactory { 13 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 14 | 15 | private final String maintainerToken; 16 | private final String adminToken; 17 | 18 | private static final TokenFactory UNIQUE_INSTANCE = new TokenFactory(); 19 | 20 | private TokenFactory() { 21 | AuthBody authBodyMaintainer = 22 | AuthBody.builder() 23 | .setUsername(CONFIG.getString("MAINTAINER_USERNAME")) 24 | .setPassword(CONFIG.getString("MAINTAINER_PASSWORD")) 25 | .build(); 26 | this.maintainerToken = getToken(authBodyMaintainer); 27 | 28 | AuthBody authBodyAdmin = 29 | AuthBody.builder() 30 | .setUsername(CONFIG.getString("ADMIN_USERNAME")) 31 | .setPassword(CONFIG.getString("ADMIN_PASSWORD")) 32 | .build(); 33 | this.adminToken = getToken(authBodyAdmin); 34 | } 35 | 36 | public static TokenFactory getInstance() { 37 | return UNIQUE_INSTANCE; 38 | } 39 | 40 | public String getTokenFor(Scope scope) { 41 | switch (scope) { 42 | case GUEST: 43 | return ""; 44 | case MAINTAINER: 45 | return maintainerToken; 46 | case ADMIN: 47 | return adminToken; 48 | default: 49 | throw new IllegalStateException( 50 | "Not a valid scope. Pick a scope from " + Arrays.toString(Scope.values())); 51 | } 52 | } 53 | 54 | private String getToken(AuthBody authBody) { 55 | Response response = 56 | given() 57 | .header("Content-Type", "application/json") 58 | .baseUri(CONFIG.getString("BASE_URL")) 59 | .body(authBody) 60 | .log() 61 | .ifValidationFails() 62 | .when() 63 | .post(CONFIG.getString("AUTH_ENDPOINT")) 64 | .then() 65 | .log() 66 | .ifError() 67 | .extract() 68 | .response(); 69 | 70 | return response.body().jsonPath().getString("token"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/basespec/SpecFactory.java: -------------------------------------------------------------------------------- 1 | package org.powertester.basespec; 2 | 3 | import static org.powertester.auth.Scope.ADMIN; 4 | import static org.powertester.auth.Scope.MAINTAINER; 5 | 6 | import com.typesafe.config.Config; 7 | import io.restassured.builder.RequestSpecBuilder; 8 | import io.restassured.config.LogConfig; 9 | import io.restassured.config.RestAssuredConfig; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import org.powertester.auth.Scope; 13 | import org.powertester.auth.TokenFactory; 14 | import org.powertester.config.TestConfig; 15 | 16 | public class SpecFactory { 17 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 18 | 19 | public static RequestSpecBuilder getSpecFor(Scope scope) { 20 | switch (scope) { 21 | case GUEST: 22 | return get(); 23 | case MAINTAINER: 24 | return get(TokenFactory.getInstance().getTokenFor(MAINTAINER)); 25 | case ADMIN: 26 | return get(TokenFactory.getInstance().getTokenFor(ADMIN)); 27 | default: 28 | throw new IllegalStateException( 29 | "Not a valid scope. Pick a scope from " + Arrays.toString(Scope.values())); 30 | } 31 | } 32 | 33 | private static RequestSpecBuilder get() { 34 | return new RequestSpecBuilder() 35 | .addHeader("Content-Type", "application/json") 36 | .addHeader("Accept", "application/json") 37 | .setBaseUri(CONFIG.getString("BASE_URL")); 38 | } 39 | 40 | private static RequestSpecBuilder get(String token) { 41 | return get() 42 | .addHeader("Cookie", "token=" + token) 43 | .addHeader("Authorization", "some-value") 44 | .setConfig( 45 | RestAssuredConfig.config() 46 | .logConfig( 47 | LogConfig.logConfig().blacklistHeaders(List.of("Cookie", "Authorization")))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/booking/Booking.java: -------------------------------------------------------------------------------- 1 | package org.powertester.booking; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import java.util.concurrent.TimeUnit; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.extern.slf4j.Slf4j; 11 | import net.datafaker.Faker; 12 | import org.powertester.booking.entitites.Bookingdates; 13 | 14 | @Slf4j 15 | @JsonInclude(JsonInclude.Include.NON_NULL) 16 | @Data 17 | @Builder(setterPrefix = "set") 18 | @Accessors(chain = true) 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | public class Booking { 22 | private String firstname; 23 | private String lastname; 24 | private long totalprice; 25 | private boolean depositpaid; 26 | private Bookingdates bookingdates; 27 | private String additionalneeds; 28 | 29 | public static Booking getInstance() { 30 | Bookingdates bookingdates = 31 | Bookingdates.builder() 32 | .setCheckin(new Faker().date().future(1, TimeUnit.DAYS, "YYYY-MM-dd")) 33 | .setCheckout(new Faker().date().future(6, TimeUnit.DAYS, "YYYY-MM-dd")) 34 | .build(); 35 | 36 | Booking booking = 37 | Booking.builder() 38 | .setFirstname(new Faker().name().firstName()) 39 | .setLastname(new Faker().name().lastName()) 40 | .setTotalprice(new Faker().number().numberBetween(1, 1000)) 41 | .setDepositpaid(true) 42 | .setBookingdates(bookingdates) 43 | .setAdditionalneeds(new Faker().food().dish()) 44 | .build(); 45 | 46 | log.info("bookingBody: {}", booking); 47 | return booking; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/booking/BookingAPI.java: -------------------------------------------------------------------------------- 1 | package org.powertester.booking; 2 | 3 | import static io.restassured.RestAssured.given; 4 | 5 | import com.typesafe.config.Config; 6 | import io.restassured.RestAssured; 7 | import io.restassured.response.Response; 8 | import org.powertester.auth.Scope; 9 | import org.powertester.basespec.SpecFactory; 10 | import org.powertester.config.TestConfig; 11 | 12 | public class BookingAPI { 13 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 14 | private Scope scope; 15 | 16 | private BookingAPI(Scope scope) { 17 | this.scope = scope; 18 | } 19 | 20 | public static BookingAPI useAs(Scope scope) { 21 | return new BookingAPI(scope); 22 | } 23 | 24 | public Response newBooking(Booking booking) { 25 | return RestAssured.given() 26 | .spec(SpecFactory.getSpecFor(scope).build()) 27 | .body(booking) 28 | .log() 29 | .ifValidationFails() 30 | .when() 31 | .post(CONFIG.getString("BOOKING_ENDPOINT")) 32 | .then() 33 | .log() 34 | .ifError() 35 | .extract() 36 | .response(); 37 | } 38 | 39 | public Response getBooking(Long bookingId) { 40 | return RestAssured.given() 41 | .spec(SpecFactory.getSpecFor(scope).build()) 42 | .log() 43 | .ifValidationFails() 44 | .when() 45 | .get(CONFIG.getString("BOOKING_ID_ENDPOINT"), bookingId) 46 | .then() 47 | .log() 48 | .ifError() 49 | .extract() 50 | .response(); 51 | } 52 | 53 | public Response updateBooking(Booking booking, Long bookingId) { 54 | // Cookie: token={{auth_token}} 55 | return given() 56 | .spec(SpecFactory.getSpecFor(scope).build()) 57 | .body(booking) 58 | .log() 59 | .ifValidationFails() 60 | .when() 61 | .put(CONFIG.getString("BOOKING_ID_ENDPOINT"), bookingId) 62 | .then() 63 | .log() 64 | .ifError() 65 | .extract() 66 | .response(); 67 | } 68 | 69 | public Response patchBooking(Booking booking, Long bookingId) { 70 | // Cookie: token={{auth_token}} 71 | return given() 72 | .spec(SpecFactory.getSpecFor(scope).build()) 73 | .body(booking) 74 | .log() 75 | .ifValidationFails() 76 | .when() 77 | .patch(CONFIG.getString("BOOKING_ID_ENDPOINT"), bookingId) 78 | .then() 79 | .log() 80 | .ifError() 81 | .extract() 82 | .response(); 83 | } 84 | 85 | public Response deleteBooking(Long bookingId) { 86 | // Cookie: token={{auth_token}} 87 | return given() 88 | .spec(SpecFactory.getSpecFor(scope).build()) 89 | .log() 90 | .ifValidationFails() 91 | .when() 92 | .delete(CONFIG.getString("BOOKING_ID_ENDPOINT"), bookingId) 93 | .then() 94 | .log() 95 | .ifError() 96 | .extract() 97 | .response(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/booking/BookingResponse.java: -------------------------------------------------------------------------------- 1 | package org.powertester.booking; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | @Data 8 | public class BookingResponse { 9 | private long bookingid; 10 | private Booking booking; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/booking/entitites/Bookingdates.java: -------------------------------------------------------------------------------- 1 | package org.powertester.booking.entitites; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | @Slf4j 10 | @Data 11 | @NoArgsConstructor 12 | @Builder(setterPrefix = "set") 13 | @AllArgsConstructor 14 | public class Bookingdates { 15 | private String checkin; 16 | private String checkout; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/config/TestConfig.java: -------------------------------------------------------------------------------- 1 | package org.powertester.config; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import java.io.File; 6 | import java.nio.file.Paths; 7 | import java.util.Objects; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.slf4j.MDC; 10 | 11 | /** 12 | * Env configuration once loaded, is to remain constant for all classes using it. Thus we will 13 | * follow Singleton design pattern here. For future reference on this topic: 14 | * https://github.com/lightbend/config 15 | */ 16 | @Slf4j 17 | public class TestConfig { 18 | /** 19 | * With this approach, we are relying on JVM to create the unique instance of TestEnvFactory when 20 | * the class is loaded. The JVM guarantees that the instance will be created before any thread 21 | * accesses the static uniqueInstance variable. This code is thus guaranteed to be thread safe. 22 | */ 23 | private static final TestConfig UNIQUE_INSTANCE = new TestConfig(); 24 | 25 | private Config config; 26 | 27 | private TestConfig() { 28 | MDC.put("testContext", "Set TestEnv Config"); 29 | config = setConfig(); 30 | MDC.clear(); 31 | } 32 | 33 | public static TestConfig getInstance() { 34 | return UNIQUE_INSTANCE; 35 | } 36 | 37 | public Config getConfig() { 38 | return config; 39 | } 40 | 41 | private Config setConfig() { 42 | log.info("Call setConfig only once for the whole test run!"); 43 | 44 | // Standard config load behavior (loads common config from application.conf file) 45 | // https://github.com/lightbend/config#standard-behavior 46 | config = ConfigFactory.load(); 47 | 48 | Config choicesConfig = ConfigFactory.load("choices"); 49 | config = config.withFallback(choicesConfig); 50 | 51 | config = getAllConfigFromFilesInTheResourcePath("common"); 52 | 53 | TestEnv testEnv = TestEnv.getEnumByValue(config.getString("TEST_ENV")); 54 | return getAllConfigFromFilesInTheResourcePath(testEnv.getValue()); 55 | } 56 | 57 | private Config getAllConfigFromFilesInTheResourcePath(String resourceBasePath) { 58 | try { 59 | for (File file : 60 | Objects.requireNonNull( 61 | Paths.get("src/main/resources/" + resourceBasePath).toFile().listFiles())) { 62 | log.info("file path: {}", file); 63 | 64 | Config childConfig = 65 | ConfigFactory.load(String.format("%s/%s", resourceBasePath, file.getName())); 66 | config = config.withFallback(childConfig); 67 | } 68 | 69 | return config; 70 | } catch (Exception exception) { 71 | exception.printStackTrace(); 72 | throw new IllegalStateException("Could not parse config. Got issues in parsing File path."); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/config/TestEnv.java: -------------------------------------------------------------------------------- 1 | package org.powertester.config; 2 | 3 | public enum TestEnv { 4 | LOCALHOST("localhost"), 5 | DEVELOP("develop"), 6 | STAGING("staging"); 7 | 8 | TestEnv(String value) { 9 | this.value = value; 10 | } 11 | 12 | private String value; 13 | 14 | public String getValue() { 15 | return value; 16 | } 17 | 18 | public static TestEnv getEnumByValue(String value) { 19 | for (TestEnv testEnv : TestEnv.values()) { 20 | if (testEnv.getValue().equalsIgnoreCase(value)) { 21 | return testEnv; 22 | } 23 | } 24 | 25 | throw new IllegalStateException("No enum constant with value: " + value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/data/TestData.java: -------------------------------------------------------------------------------- 1 | package org.powertester.data; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | 8 | public class TestData { 9 | private Map keyValueMap; // Ex: amount: 100, 200, 300 10 | private Map keyTypeMap; // Ex: amount: output, input, io 11 | 12 | public TestData(Map keyTypeMap, Map keyValueMap) { 13 | this.keyTypeMap = keyTypeMap; 14 | this.keyValueMap = keyValueMap; 15 | } 16 | 17 | public TestData() { 18 | this(new HashMap<>(), new HashMap<>()); 19 | } 20 | 21 | public void setKey(String key, String value, String type) { 22 | keyValueMap.put(key, value); 23 | keyTypeMap.put(key, type); 24 | } 25 | 26 | public void setKey(String key, String value) { 27 | setKey(key, value, "default"); 28 | } 29 | 30 | // Get trimmed keys 31 | public Set getKeys() { 32 | return keyValueMap.keySet().stream().map(String::trim).collect(Collectors.toSet()); 33 | } 34 | 35 | public Map getKeyTypeMap() { 36 | return keyTypeMap; 37 | } 38 | 39 | public Map getKeyValueMap() { 40 | return keyValueMap; 41 | } 42 | 43 | public void setKeyValueMap(Map keyValueMap) { 44 | this.keyValueMap = keyValueMap; 45 | } 46 | 47 | public void setKeyTypeMap(Map keyTypeMap) { 48 | this.keyTypeMap = keyTypeMap; 49 | } 50 | 51 | public String getValue(String key) { 52 | return keyValueMap.get(key); 53 | } 54 | 55 | public String getType(String key) { 56 | return keyTypeMap.get(key); 57 | } 58 | 59 | public String toString() { 60 | return "keyValueMap: " + keyValueMap + "\nkeyTypeMap: " + keyTypeMap; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/database/DBConnection.java: -------------------------------------------------------------------------------- 1 | package org.powertester.database; 2 | 3 | import static org.junit.Assert.fail; 4 | 5 | import com.typesafe.config.Config; 6 | import com.zaxxer.hikari.HikariDataSource; 7 | import java.sql.CallableStatement; 8 | import java.sql.Connection; 9 | import java.sql.PreparedStatement; 10 | import java.sql.ResultSet; 11 | import java.sql.ResultSetMetaData; 12 | import java.sql.SQLException; 13 | import java.sql.Statement; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import lombok.extern.slf4j.Slf4j; 19 | import org.powertester.config.TestConfig; 20 | 21 | @Slf4j 22 | public class DBConnection { 23 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 24 | private static final String DB_URL = CONFIG.getString("DB_URL"); 25 | private static final String DB_USER = CONFIG.getString("DB_USER"); 26 | private static final String DB_PASSWORD = CONFIG.getString("DB_PASSWORD"); 27 | private static final DBConnection INSTANCE = new DBConnection(); 28 | 29 | private HikariDataSource dataSource; 30 | 31 | private DBConnection() { 32 | dataSource = getDataSource(); 33 | } 34 | 35 | public static DBConnection getInstance() { 36 | return INSTANCE; 37 | } 38 | 39 | private HikariDataSource getDataSource() { 40 | try { 41 | if (dataSource == null) { 42 | dataSource = new HikariDataSource(); 43 | dataSource.setJdbcUrl(DB_URL); 44 | dataSource.setUsername(DB_USER); 45 | dataSource.setPassword(DB_PASSWORD); 46 | dataSource.setMaximumPoolSize(20); // 20 connections 47 | dataSource.setMinimumIdle(10); // 10 connections 48 | dataSource.setConnectionTimeout(30000); // 30 seconds 49 | dataSource.setIdleTimeout(30000); // 30 seconds 50 | dataSource.setMaxLifetime(1800000); // 30 minutes 51 | dataSource.setLeakDetectionThreshold(30000); // 30 seconds 52 | dataSource.setPoolName("PowerTester"); 53 | } 54 | } catch (Exception e) { 55 | log.error("Error initializing Hikari datasource", e); 56 | log.error("⚠ Cancelling test run since tests depend on Database Connection"); 57 | System.exit(1); 58 | } 59 | 60 | log.info("Hikari datasource initialized"); 61 | return dataSource; 62 | } 63 | 64 | public Connection getConnection() throws SQLException { 65 | Connection connection = dataSource.getConnection(); 66 | try (Statement statement = connection.createStatement()) { 67 | statement.execute(CONFIG.getString("QUERY_TO_SET_SCHEMA_USER")); 68 | statement.execute(CONFIG.getString("QUERY_TO_SET_DATE_FORMAT")); 69 | } catch (Exception e) { 70 | throw new IllegalStateException("Error setting schema and date format", e); 71 | } 72 | return connection; 73 | } 74 | 75 | // Execute update query 76 | public void executeUpdate(String sql) { 77 | try (Connection connection = getConnection(); 78 | Statement statement = connection.createStatement()) { 79 | statement.executeUpdate(sql); 80 | } catch (Exception e) { 81 | throw new IllegalStateException("Error executing update query" + sql, e); 82 | } 83 | } 84 | 85 | // Preferred option 1: Execute a prepared statement and return the resultSet data as a list of map 86 | // of column name and value 87 | public List> executePreparedStatement(String sql, String... parameters) { 88 | try (Connection connection = getConnection(); 89 | PreparedStatement statement = connection.prepareStatement(sql)) { 90 | 91 | // Set the parameters 92 | int parameterIndex = 1; 93 | for (String parameter : parameters) { 94 | statement.setObject(parameterIndex++, parameter); 95 | } 96 | 97 | try (ResultSet resultSet = statement.executeQuery()) { 98 | return getResultListFromResultSet(resultSet); 99 | } 100 | } catch (Exception e) { 101 | throw new IllegalStateException("Error executing prepared statement" + sql, e); 102 | } 103 | } 104 | 105 | private static List> getResultListFromResultSet(ResultSet resultSet) 106 | throws SQLException { 107 | List> resultList = new ArrayList<>(); 108 | 109 | ResultSetMetaData metaData = resultSet.getMetaData(); 110 | int columnCount = metaData.getColumnCount(); 111 | while (resultSet.next()) { 112 | Map row = new HashMap<>(); 113 | for (int i = 1; i <= columnCount; i++) { 114 | row.put(metaData.getColumnName(i), resultSet.getString(i)); 115 | } 116 | resultList.add(row); 117 | } 118 | return resultList; 119 | } 120 | 121 | // Run stored procedure with parameters 122 | public Map runStoredProcedure(String sql, String... outputParameters) { 123 | try (Connection connection = getConnection(); 124 | CallableStatement statement = connection.prepareCall(sql)) { 125 | 126 | // Register output parameters 127 | int parameterIndex = 1; 128 | for (String outputParameter : outputParameters) { 129 | statement.setObject(parameterIndex++, outputParameter); 130 | } 131 | 132 | // Execute stored procedure 133 | statement.execute(); 134 | 135 | // Get output parameters 136 | Map resultMap = new HashMap<>(); 137 | for (String outputParameter : outputParameters) { 138 | resultMap.put(outputParameter, statement.getString(outputParameter)); 139 | } 140 | 141 | // Print result and return 142 | log.info("Stored procedure result: {}", resultMap); 143 | 144 | // Return map if not empty. Otherwise, fail this method. 145 | if (resultMap.isEmpty()) { 146 | fail("Stored procedure returned empty result"); 147 | } 148 | 149 | return resultMap; 150 | } catch (Exception e) { 151 | throw new IllegalStateException("Error executing stored procedure" + sql, e); 152 | } 153 | } 154 | 155 | // Close connection pool 156 | public void closeConnectionPool() { 157 | log.info("Closing Hikari datasource pool (only once) at the end of the whole test run"); 158 | dataSource.close(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/LoggingExtension.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions; 2 | 3 | import org.junit.jupiter.api.extension.AfterEachCallback; 4 | import org.junit.jupiter.api.extension.BeforeEachCallback; 5 | import org.junit.jupiter.api.extension.ExtensionContext; 6 | import org.slf4j.MDC; 7 | 8 | public class LoggingExtension implements BeforeEachCallback, AfterEachCallback { 9 | 10 | @Override 11 | public void beforeEach(ExtensionContext context) { 12 | MDC.put("testContext", context.getDisplayName().replace("()", "")); 13 | } 14 | 15 | @Override 16 | public void afterEach(ExtensionContext context) { 17 | MDC.clear(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/ReportingExtension.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions; 2 | 3 | import com.typesafe.config.Config; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 6 | import org.junit.jupiter.api.extension.ExtensionContext; 7 | import org.powertester.config.TestConfig; 8 | import org.powertester.extensions.report.PublishResults; 9 | import org.powertester.extensions.report.TestRunMetaData; 10 | 11 | @Slf4j 12 | public class ReportingExtension implements AfterTestExecutionCallback { 13 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 14 | private static final Boolean PUBLISH_RESULTS_TO_ELASTIC = 15 | CONFIG.getBoolean("PUBLISH_RESULTS_TO_ELASTIC"); 16 | 17 | @Override 18 | public void afterTestExecution(ExtensionContext context) throws Exception { 19 | if (PUBLISH_RESULTS_TO_ELASTIC) { 20 | log.info("publishing results to elastic"); 21 | TestRunMetaData testRunMetaData = new TestRunMetaData().setBody(context); 22 | PublishResults.toElastic(testRunMetaData); 23 | } else { 24 | log.info("PUBLISH_RESULTS_TO_ELASTIC flag is set to false"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/TestRunExtension.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions; 2 | 3 | import static org.powertester.utils.DateUtils.getDateAsString; 4 | 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.Paths; 8 | import java.util.Locale; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.junit.jupiter.api.extension.BeforeAllCallback; 12 | import org.junit.jupiter.api.extension.ExtensionContext; 13 | import org.powertester.database.DBConnection; 14 | import org.slf4j.MDC; 15 | 16 | @Slf4j 17 | public class TestRunExtension 18 | implements BeforeAllCallback, ExtensionContext.Store.CloseableResource { 19 | 20 | private final AtomicBoolean runExecuted = new AtomicBoolean(false); 21 | private static final Path TEST_REPORT_PATH = 22 | Paths.get(".", "test-reports", getDateAsString("yyyy-MM-dd/HH-mm")); 23 | private long testRunStartTime; 24 | 25 | @Override 26 | public void beforeAll(final ExtensionContext extensionContext) { 27 | try { 28 | // Execute the method logic only if it hasn't been executed before 29 | if (!runExecuted.get() && runExecuted.compareAndSet(false, true)) { 30 | MDC.put("testContext", "Onetime TestRun Setup"); 31 | log.info("Test run started."); 32 | 33 | testRunStartTime = System.currentTimeMillis(); 34 | 35 | Locale.setDefault(Locale.ENGLISH); 36 | 37 | // Create a new instance of DBConnection so that it can be used for the entire test run. 38 | DBConnection.getInstance(); 39 | 40 | Files.createDirectories(TEST_REPORT_PATH); 41 | 42 | // The following line registers a callback hook to be executed when the root test context is 43 | // shut down. 44 | extensionContext 45 | .getRoot() 46 | .getStore(ExtensionContext.Namespace.GLOBAL) 47 | .put("TestRunExtension", this); 48 | } 49 | } catch (Exception e) { 50 | log.error("Error initializing TestRunExtension", e); 51 | log.error("⚠ Cancelling test run since tests depend on TestRunExtension"); 52 | System.exit(1); 53 | } finally { 54 | MDC.clear(); 55 | } 56 | } 57 | 58 | @Override 59 | public void close() { 60 | MDC.put("testContext", "TestRun Completed"); 61 | DBConnection.getInstance().closeConnectionPool(); 62 | 63 | log.info( 64 | "Test run completed in {} seconds.", 65 | (System.currentTimeMillis() - testRunStartTime) / 1000.0); 66 | MDC.clear(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/TimingExtension.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.junit.jupiter.api.extension.*; 5 | 6 | @Slf4j 7 | public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { 8 | private static ThreadLocal testExecutionTimeThread = new ThreadLocal<>(); 9 | private long startTimeMethod; 10 | 11 | public static Double getTestExecutionTimeThread() { 12 | return testExecutionTimeThread.get(); 13 | } 14 | 15 | public static void removeTestExecutionTimeThread() { 16 | testExecutionTimeThread.remove(); 17 | } 18 | 19 | @Override 20 | public void beforeTestExecution(ExtensionContext context) { 21 | startTimeMethod = System.currentTimeMillis(); 22 | } 23 | 24 | @Override 25 | public void afterTestExecution(ExtensionContext context) { 26 | double duration = (System.currentTimeMillis() - startTimeMethod) / 1000.0; 27 | log.info("Test took {} seconds.", duration); 28 | 29 | // Now set this variable as a thread local so that it can be used for reporting total execution 30 | // time. 31 | testExecutionTimeThread.set(duration); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/report/ElasticLowLevelRestClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions.report; 2 | 3 | import co.elastic.clients.transport.TransportUtils; 4 | import com.typesafe.config.Config; 5 | import javax.net.ssl.SSLContext; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.http.Header; 8 | import org.apache.http.HttpHost; 9 | import org.apache.http.auth.AuthScope; 10 | import org.apache.http.auth.UsernamePasswordCredentials; 11 | import org.apache.http.impl.client.BasicCredentialsProvider; 12 | import org.apache.http.message.BasicHeader; 13 | import org.elasticsearch.client.RestClient; 14 | import org.powertester.config.TestConfig; 15 | 16 | @Slf4j 17 | public class ElasticLowLevelRestClientFactory { 18 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 19 | 20 | public static RestClient getRestClient(ElasticServerChoices ELASTIC_SERVER) { 21 | log.info("Getting client for ELASTIC_SERVER: {}", ELASTIC_SERVER); 22 | switch (ELASTIC_SERVER) { 23 | case ON_CLOUD: 24 | return getRestClientForCloud(); 25 | case ON_LOCALHOST_SECURE: 26 | return getRestClientForLocalhostHTTPS(); 27 | case ON_LOCALHOST_INSECURE: 28 | return getRestClientForLocalhostHTTP(); 29 | default: 30 | throw new IllegalStateException( 31 | String.format( 32 | "%s is not a valid HOST choice. Pick your HOST from %s.", 33 | ELASTIC_SERVER, java.util.Arrays.asList(ElasticServerChoices.values()))); 34 | } 35 | } 36 | 37 | private static RestClient getRestClientForCloud() { 38 | final String ELASTIC_HOST = CONFIG.getString("ON_CLOUD.ELASTIC_HOST"); 39 | final int ELASTIC_PORT = CONFIG.getInt("ON_CLOUD.ELASTIC_PORT"); 40 | final String ELASTIC_API_KEY = CONFIG.getString("ON_CLOUD.ELASTIC_API_KEY"); 41 | 42 | Header[] headers = 43 | new Header[] { 44 | new BasicHeader("Accept", "application/json"), 45 | new BasicHeader("Authorization", "ApiKey " + ELASTIC_API_KEY) 46 | }; 47 | 48 | // tag::create-secure-client-fingerprint 49 | return RestClient.builder(new HttpHost(ELASTIC_HOST, ELASTIC_PORT, "https")) // <3> 50 | .setDefaultHeaders(headers) 51 | .build(); 52 | } 53 | 54 | // For elastic version >=8, default mode is HTTPS. Versions less than 8 have default mode HTTP. 55 | private static RestClient getRestClientForLocalhostHTTP() { 56 | final String ELASTIC_HOST = CONFIG.getString("ON_LOCALHOST_INSECURE.ELASTIC_HOST"); 57 | final int ELASTIC_PORT = CONFIG.getInt("ON_LOCALHOST_INSECURE.ELASTIC_PORT"); 58 | 59 | return RestClient.builder(new HttpHost(ELASTIC_HOST, ELASTIC_PORT, "http")) // <3> 60 | .build(); 61 | } 62 | 63 | // For elastic version >=8, default mode is HTTPS. Versions less than 8 have default mode HTTP. 64 | private static RestClient getRestClientForLocalhostHTTPS() { 65 | final String ELASTIC_HOST = CONFIG.getString("ON_LOCALHOST_SECURE.ELASTIC_HOST"); 66 | final int ELASTIC_PORT = CONFIG.getInt("ON_LOCALHOST_SECURE.ELASTIC_PORT"); 67 | 68 | // tag::create-secure-client-fingerprint 69 | final String ELASTIC_FINGERPRINT = CONFIG.getString("ON_LOCALHOST_SECURE.ELASTIC_FINGERPRINT"); 70 | SSLContext sslContext = TransportUtils.sslContextFromCaFingerprint(ELASTIC_FINGERPRINT); // <1> 71 | 72 | final String ELASTIC_LOGIN = CONFIG.getString("ON_LOCALHOST_SECURE.ELASTIC_LOGIN"); 73 | final String ELASTIC_PASSWORD = CONFIG.getString("ON_LOCALHOST_SECURE.ELASTIC_PASSWORD"); 74 | BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); // <2> 75 | credsProv.setCredentials( 76 | AuthScope.ANY, new UsernamePasswordCredentials(ELASTIC_LOGIN, ELASTIC_PASSWORD)); 77 | 78 | return RestClient.builder(new HttpHost(ELASTIC_HOST, ELASTIC_PORT, "https")) // <3> 79 | .setHttpClientConfigCallback( 80 | hc -> 81 | hc.setSSLContext(sslContext) // <4> 82 | .setDefaultCredentialsProvider(credsProv)) 83 | .build(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/report/ElasticServerChoices.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions.report; 2 | 3 | public enum ElasticServerChoices { 4 | ON_CLOUD("ON_CLOUD"), 5 | ON_LOCALHOST_SECURE("ON_LOCALHOST_SECURE"), 6 | ON_LOCALHOST_INSECURE("ON_LOCALHOST_INSECURE"); 7 | 8 | ElasticServerChoices(String value) { 9 | this.value = value; 10 | } 11 | 12 | private String value; 13 | 14 | public String getValue() { 15 | return value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/report/PublishResults.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions.report; 2 | 3 | import co.elastic.clients.elasticsearch.ElasticsearchClient; 4 | import co.elastic.clients.elasticsearch.core.IndexResponse; 5 | import co.elastic.clients.json.jackson.JacksonJsonpMapper; 6 | import co.elastic.clients.transport.ElasticsearchTransport; 7 | import co.elastic.clients.transport.rest_client.RestClientTransport; 8 | import com.typesafe.config.Config; 9 | import java.io.IOException; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.elasticsearch.client.RestClient; 12 | import org.powertester.config.TestConfig; 13 | 14 | @Slf4j 15 | public class PublishResults { 16 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 17 | 18 | private static final ElasticServerChoices ELASTIC_SERVER = 19 | CONFIG.getEnum(ElasticServerChoices.class, "ELASTIC_SERVER"); 20 | private static final String ELASTIC_INDEX = getStringConfig("ELASTIC_INDEX"); 21 | 22 | private static final ElasticsearchClient ELASTICSEARCH_CLIENT = 23 | getElasticHighLevelRestAPIClient(); 24 | 25 | public static void toElastic(TestRunMetaData testRunMetaData) throws IOException { 26 | IndexResponse response = 27 | ELASTICSEARCH_CLIENT.index(i -> i.index(ELASTIC_INDEX).document(testRunMetaData)); 28 | 29 | log.info("Indexed with version " + response.version()); 30 | } 31 | 32 | public static ElasticsearchClient getElasticHighLevelRestAPIClient() { 33 | log.info("creating elastic client"); 34 | 35 | RestClient restClient = ElasticLowLevelRestClientFactory.getRestClient(ELASTIC_SERVER); 36 | 37 | // Create the transport with a Jackson mapper 38 | ElasticsearchTransport transport = 39 | new RestClientTransport(restClient, new JacksonJsonpMapper()); 40 | 41 | // And create the API client 42 | return new ElasticsearchClient(transport); 43 | } 44 | 45 | private static String getStringConfig(String configName) { 46 | return CONFIG.getString(ELASTIC_SERVER.getValue() + "." + configName); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/extensions/report/TestRunMetaData.java: -------------------------------------------------------------------------------- 1 | package org.powertester.extensions.report; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.typesafe.config.Config; 6 | import java.time.LocalDateTime; 7 | import java.time.ZoneId; 8 | import java.util.Arrays; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import net.datafaker.Faker; 14 | import org.junit.jupiter.api.extension.ExtensionContext; 15 | import org.powertester.config.TestConfig; 16 | import org.powertester.extensions.TimingExtension; 17 | 18 | @Slf4j 19 | @JsonInclude(JsonInclude.Include.NON_NULL) 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | @Data 23 | public class TestRunMetaData { 24 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 25 | 26 | private static final String RUN_TIME = LocalDateTime.now(ZoneId.of("UTC")).toString(); 27 | 28 | private static final String RUN_NAME = new Faker().funnyName().name(); 29 | 30 | private static final String TRIGGERED_BY = getTriggeredBy(); 31 | 32 | /** 33 | * Note: Jackson would ignore all above static variables when creating a JSON object to push to 34 | * Elastic; and will only consider below "fields" to create a Json data to publish. 35 | */ 36 | private final String project = "restpro"; 37 | 38 | private final String testEnvironment = CONFIG.getString("TEST_ENV"); 39 | 40 | @JsonProperty("run-time") 41 | private String runTime; 42 | 43 | @JsonProperty("run-name") 44 | private String runName; 45 | 46 | @JsonProperty("triggered-by") 47 | private String triggeredBy; 48 | 49 | @JsonProperty("test-class") 50 | private String testClass; 51 | 52 | @JsonProperty("test-name") 53 | private String testName; 54 | 55 | @JsonProperty("test-type") 56 | private String testType; 57 | 58 | private String status; 59 | 60 | private String reason; 61 | 62 | @JsonProperty("time (Sec)") 63 | private String duration; 64 | 65 | public TestRunMetaData setBody(ExtensionContext context) { 66 | runTime = RUN_TIME; 67 | runName = RUN_NAME; 68 | triggeredBy = TRIGGERED_BY; 69 | 70 | testClass = context.getTestClass().orElseThrow().getSimpleName(); 71 | testName = context.getDisplayName(); 72 | 73 | testType = getTestType(context); 74 | 75 | duration = getTestDuration(); 76 | 77 | setTestStatusAndReason(context); 78 | 79 | return this; 80 | } 81 | 82 | private String getTestDuration() { 83 | String testDuration; 84 | if (TimingExtension.getTestExecutionTimeThread() >= 5) { 85 | testDuration = TimingExtension.getTestExecutionTimeThread() + " ⏰"; 86 | } else { 87 | testDuration = String.valueOf(TimingExtension.getTestExecutionTimeThread()); 88 | } 89 | 90 | TimingExtension.removeTestExecutionTimeThread(); 91 | log.info("testDuration {}", testDuration); 92 | 93 | return testDuration; 94 | } 95 | 96 | private void setTestStatusAndReason(ExtensionContext context) { 97 | boolean testStatus = context.getExecutionException().isPresent(); 98 | if (testStatus) { 99 | status = "❌"; 100 | reason = "🐞 " + context.getExecutionException().toString(); 101 | } else { 102 | status = "✅"; 103 | reason = "🌻"; 104 | } 105 | } 106 | 107 | private static String getTriggeredBy() { 108 | if (CONFIG.getString("TRIGGERED_BY").isEmpty()) { 109 | return System.getProperty("user.name"); 110 | } else { 111 | return CONFIG.getString("TRIGGERED_BY"); 112 | } 113 | } 114 | 115 | // Set test-type based on annotation at the class level 116 | private String getTestType(ExtensionContext context) { 117 | return context 118 | .getTestClass() 119 | .flatMap( 120 | clazz -> 121 | Arrays.stream(clazz.getAnnotations()) 122 | .filter( 123 | annotation -> 124 | annotation.annotationType().getSimpleName().contains("Test") 125 | || annotation.annotationType().getSimpleName().contains("Check")) 126 | .map(annotation -> annotation.annotationType().getSimpleName()) 127 | .findFirst()) 128 | .orElse("undefined"); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/healthcheck/HealthCheckAPI.java: -------------------------------------------------------------------------------- 1 | package org.powertester.healthcheck; 2 | 3 | import static io.restassured.RestAssured.given; 4 | import static org.powertester.auth.Scope.GUEST; 5 | 6 | import com.typesafe.config.Config; 7 | import io.restassured.response.Response; 8 | import org.powertester.basespec.SpecFactory; 9 | import org.powertester.config.TestConfig; 10 | 11 | public class HealthCheckAPI { 12 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 13 | 14 | public static Response healthCheck() { 15 | // Cookie: token={{auth_token}} 16 | return given() 17 | .spec(SpecFactory.getSpecFor(GUEST).build()) 18 | .log() 19 | .ifValidationFails() 20 | .when() 21 | .get(CONFIG.getString("HEALTHCHECK_ENDPOINT")) 22 | .then() 23 | .log() 24 | .ifError() 25 | .extract() 26 | .response(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/powertester/utils/DateUtils.java: -------------------------------------------------------------------------------- 1 | package org.powertester.utils; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.ZoneOffset; 5 | import java.time.format.DateTimeFormatter; 6 | 7 | public class DateUtils { 8 | private DateUtils() { 9 | throw new IllegalStateException("Utility class"); 10 | } 11 | 12 | public static String getDateAsString(String format) { 13 | LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); 14 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); 15 | return now.format(formatter); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension: -------------------------------------------------------------------------------- 1 | org.powertester.extensions.ReportingExtension 2 | org.powertester.extensions.TimingExtension 3 | org.powertester.extensions.TestRunExtension 4 | org.powertester.extensions.LoggingExtension 5 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # This is the place where you put all your common configuration that does not change with test environment 2 | 3 | # Endpoints can come here (endpoints starts with a forward slash and ends without a forward slash) 4 | HEALTHCHECK_ENDPOINT = "/ping" 5 | AUTH_ENDPOINT = "/auth" 6 | BOOKING_ENDPOINT = "/booking" 7 | 8 | BOOKING_ID_ENDPOINT = ${BOOKING_ENDPOINT}"/{bookingId}" 9 | 10 | ADMIN_ENDPOINT = "/admin" 11 | 12 | TOGGLE = false 13 | NR_OF_USERS = 10 14 | PRICE = 123.456 15 | 16 | # Awaitility configuration 17 | AWAITILITY_TIMEOUT_IN_SECONDS = 60 18 | AWAITILITY_POLL_INTERVAL_IN_SECONDS = 1 19 | AWAITILITY_POLL_DELAY_IN_SECONDS = 0 20 | -------------------------------------------------------------------------------- /src/main/resources/choices.conf: -------------------------------------------------------------------------------- 1 | # You can put all your user choices here 2 | 3 | # Your valid env choices should be one of [LOCALHOST, DEVELOP, STAGING] 4 | TEST_ENV = "STAGING" 5 | 6 | # Elastic choices 7 | PUBLISH_RESULTS_TO_ELASTIC = false # to publish or not to publish 8 | ELASTIC_SERVER = "ON_CLOUD" # server choices (ON_CLOUD, ON_LOCALHOST_INSECURE, ON_LOCALHOST_SECURE) 9 | -------------------------------------------------------------------------------- /src/main/resources/common/secrets.conf: -------------------------------------------------------------------------------- 1 | # Elastic cloud host details 2 | TRIGGERED_BY ="" # This value should be passed from CI. 3 | RUN_NAME ="" # This value should be passed from CI. 4 | 5 | ON_CLOUD { 6 | ELASTIC_HOST = "d33b55b7f90648b08299d980ff513451.us-central1.gcp.cloud.es.io" 7 | ELASTIC_PORT = 443 8 | ELASTIC_API_KEY = "TDZpLTg0VUJzeS0ta1dWTEpLWGg6dGFzUjM3QWpSdHUweklUdDdkRTYzQQ==" 9 | ELASTIC_INDEX = "search-zero-1" # on cloud, by default, it adds "search-" to name. On local, there is no such restriction. 10 | } 11 | 12 | # Elastic localhost details (default insecure connection for elastic version lesser than 8) 13 | ON_LOCALHOST_INSECURE { 14 | ELASTIC_HOST = "localhost" 15 | ELASTIC_PORT = 9200 16 | ELASTIC_INDEX = "search-zero-1" 17 | } 18 | 19 | # Elastic localhost details (default secure connection for elastic version higher than or equal to 8) 20 | ON_LOCALHOST_SECURE { 21 | ELASTIC_HOST = "localhost" 22 | ELASTIC_PORT = 9200 23 | ELASTIC_LOGIN = "elastic" 24 | ELASTIC_PASSWORD = "iBxbdxDQR3HxEuWMr7BM" # Each time a new password is generated. Replace it here. 25 | ELASTIC_FINGERPRINT = "3781f46608ce1f4a7f0f6507f18a99839e8f12cedfdbe318c4b1889565747342" # Each time a new password is generated. Replace it here. 26 | ELASTIC_INDEX = "search-zero-1" 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/develop/secrets.conf: -------------------------------------------------------------------------------- 1 | # Environment specific secrets comes here (these will be encrypted so that only people who have access can view them) 2 | 3 | ADMIN_USERNAME = "admin" 4 | ADMIN_PASSWORD = "password123" 5 | 6 | MAINTAINER_USERNAME = "admin" 7 | MAINTAINER_PASSWORD = "password123" 8 | 9 | BASE_URL = "https://develop-restful-booker.herokuapp.com" 10 | 11 | DB_URL="" # This is the URL to the database 12 | DB_USER="" # This is the username to the database 13 | DB_PASSWORD="" # This is the password to the database 14 | -------------------------------------------------------------------------------- /src/main/resources/develop/test-data.conf: -------------------------------------------------------------------------------- 1 | # Environment specific test data comes here 2 | 3 | ADMIN_NAME = "develop-admin-user" 4 | -------------------------------------------------------------------------------- /src/main/resources/develop/user-info.conf: -------------------------------------------------------------------------------- 1 | # Environment specific test data comes here 2 | 3 | USER_NAME = "develop-user" 4 | -------------------------------------------------------------------------------- /src/main/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | # Reference : https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution 2 | 3 | # To run tests or classes in sequence, use value : same_thread. 4 | # To run tests or classes in parallel use value: concurrent 5 | junit.jupiter.execution.parallel.enabled=true 6 | junit.jupiter.execution.parallel.mode.default=concurrent 7 | junit.jupiter.execution.parallel.mode.classes.default=concurrent 8 | 9 | # https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-config 10 | # Different modes of configurations below (dynamic, fixed) 11 | # Note: First try with dynamic configuration and see at what thread count your system struggles. 12 | # Once you find that threshold, switch from dynamic mode to fixed mode. 13 | junit.jupiter.execution.parallel.config.strategy=dynamic 14 | junit.jupiter.execution.parallel.config.dynamic.factor=1 15 | 16 | # Once you find the thread count at which your system struggles, you can now fix the number of threads to that value. 17 | #junit.jupiter.execution.parallel.config.strategy = fixed 18 | #junit.jupiter.execution.parallel.config.fixed.parallelism = 4 19 | 20 | # auto detect extensions 21 | junit.jupiter.extensions.autodetection.enabled=true 22 | -------------------------------------------------------------------------------- /src/main/resources/localhost/secrets.conf: -------------------------------------------------------------------------------- 1 | # Environment specific secrets comes here (these will be encrypted so that only people who have access can view them) 2 | 3 | ADMIN_USERNAME = "admin" 4 | ADMIN_PASSWORD = "password123" 5 | 6 | MAINTAINER_USERNAME = "admin" 7 | MAINTAINER_PASSWORD = "password123" 8 | 9 | BASE_URL = "https://localhost:8080" 10 | 11 | DB_URL="" # This is the URL to the database 12 | DB_USER="" # This is the username to the database 13 | DB_PASSWORD="" # This is the password to the database 14 | -------------------------------------------------------------------------------- /src/main/resources/localhost/test-data.conf: -------------------------------------------------------------------------------- 1 | # Environment specific test data comes here 2 | 3 | ADMIN_NAME = "local-admin-user" 4 | -------------------------------------------------------------------------------- /src/main/resources/localhost/user-info.conf: -------------------------------------------------------------------------------- 1 | # Environment specific test data comes here 2 | 3 | USER_NAME = "localhost-user" 4 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | [%-5level] %d{HH:mm:ss.SSS} [%logger{0}] %X{testContext}: %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/schemas/create-booking-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "bookingid": { 6 | "type": "integer" 7 | }, 8 | "booking": { 9 | "type": "object", 10 | "properties": { 11 | "firstname": { 12 | "type": "string" 13 | }, 14 | "lastname": { 15 | "type": "string" 16 | }, 17 | "totalprice": { 18 | "type": "integer" 19 | }, 20 | "depositpaid": { 21 | "type": "boolean" 22 | }, 23 | "bookingdates": { 24 | "type": "object", 25 | "properties": { 26 | "checkin": { 27 | "type": "string" 28 | }, 29 | "checkout": { 30 | "type": "string" 31 | } 32 | }, 33 | "required": [ 34 | "checkin", 35 | "checkout" 36 | ] 37 | }, 38 | "additionalneeds": { 39 | "type": "string" 40 | } 41 | }, 42 | "required": [ 43 | "firstname", 44 | "lastname", 45 | "totalprice", 46 | "depositpaid", 47 | "bookingdates" 48 | ] 49 | } 50 | }, 51 | "required": [ 52 | "bookingid", 53 | "booking" 54 | ], 55 | "additionalProperties": false 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/schemas/read-update-booking-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "firstname": { 6 | "type": "string" 7 | }, 8 | "lastname": { 9 | "type": "string" 10 | }, 11 | "totalprice": { 12 | "type": "integer" 13 | }, 14 | "depositpaid": { 15 | "type": "boolean" 16 | }, 17 | "bookingdates": { 18 | "type": "object", 19 | "properties": { 20 | "checkin": { 21 | "type": "string" 22 | }, 23 | "checkout": { 24 | "type": "string" 25 | } 26 | }, 27 | "required": [ 28 | "checkin", 29 | "checkout" 30 | ] 31 | }, 32 | "additionalneeds": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "firstname", 38 | "lastname", 39 | "totalprice", 40 | "depositpaid", 41 | "bookingdates" 42 | ], 43 | "additionalProperties": false 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/staging/secrets.conf: -------------------------------------------------------------------------------- 1 | # Environment specific secrets comes here (these will be encrypted so that only people who have access can view them) 2 | 3 | ADMIN_USERNAME = "admin" 4 | ADMIN_PASSWORD = "password123" 5 | 6 | MAINTAINER_USERNAME = "admin" 7 | MAINTAINER_PASSWORD = "password123" 8 | 9 | BASE_URL = "https://restful-booker.herokuapp.com" 10 | 11 | DB_URL="" # This is the URL to the database 12 | DB_USER="" # This is the username to the database 13 | DB_PASSWORD="" # This is the password to the database 14 | -------------------------------------------------------------------------------- /src/main/resources/staging/test-data.conf: -------------------------------------------------------------------------------- 1 | # Environment specific test data comes here 2 | 3 | ADMIN_NAME = "staging-admin-user" 4 | -------------------------------------------------------------------------------- /src/main/resources/staging/user-info.conf: -------------------------------------------------------------------------------- 1 | # Environment specific test data comes here 2 | 3 | USER_NAME = "staging-user" 4 | -------------------------------------------------------------------------------- /src/test/java/DoNotUseRestAssuredOnlyTests.java: -------------------------------------------------------------------------------- 1 | import static io.restassured.RestAssured.given; 2 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 3 | import static org.hamcrest.Matchers.equalTo; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.powertester.annotations.FlakyTest; 7 | 8 | /** 9 | * These tests are for demo purpose only (and to show the challenges that comes with relying only on 10 | * RestAssured for writing tests. Do not use this as reference to design your tests. 11 | */ 12 | @FlakyTest 13 | public class DoNotUseRestAssuredOnlyTests { 14 | @Test 15 | // Given_When_Then 16 | public void assertThatUserCanCreateABooking() { 17 | String jsonBody = 18 | "{\n" 19 | + " \"firstname\": \"Jim\",\n" 20 | + " \"lastname\": \"Brown\",\n" 21 | + " \"totalprice\": 111,\n" 22 | + " \"depositpaid\": true,\n" 23 | + " \"bookingdates\": {\n" 24 | + " \"checkin\": \"2018-01-01\",\n" 25 | + " \"checkout\": \"2019-01-01\"\n" 26 | + " },\n" 27 | + " \"additionalneeds\": \"Breakfast\"\n" 28 | + "}"; 29 | 30 | given() 31 | .header("Content-Type", "application/json") 32 | .header("Accept", "application/json") 33 | .baseUri("https://restful-booker.herokuapp.com") 34 | .body(jsonBody) 35 | .when() 36 | .post("/booking") 37 | .then() 38 | .body(matchesJsonSchemaInClasspath("schemas/create-booking-schema.json")) 39 | .statusCode(200) 40 | .body( 41 | "booking.firstname", 42 | equalTo("Jim"), 43 | "booking.lastname", 44 | equalTo("Brown"), 45 | "booking.totalprice", 46 | equalTo(111), 47 | "booking.depositpaid", 48 | equalTo(true), 49 | "booking.bookingdates.checkin", 50 | equalTo("2018-01-01"), 51 | "booking.bookingdates.checkout", 52 | equalTo("2019-01-01") 53 | // "booking.additionalneeds", equalTo("Breakfast") 54 | ) 55 | .extract() 56 | .response() 57 | .prettyPrint(); 58 | } 59 | 60 | @Test 61 | // Given_When_Then 62 | public void assertThatUserCanCreateABookingWithOnlyMandatoryDetails() { 63 | String jsonBody = 64 | "{\n" 65 | + " \"firstname\": \"Jim\",\n" 66 | + " \"lastname\": \"Brown\",\n" 67 | + " \"totalprice\": 111,\n" 68 | + " \"depositpaid\": true,\n" 69 | + " \"bookingdates\": {\n" 70 | + " \"checkin\": \"2018-01-01\",\n" 71 | + " \"checkout\": \"2019-01-01\"\n" 72 | + " }\n" 73 | + "}"; 74 | 75 | given() 76 | .header("Content-Type", "application/json") 77 | .header("Accept", "application/json") 78 | .baseUri("https://restful-booker.herokuapp.com") 79 | .body(jsonBody) 80 | .when() 81 | .post("/booking") 82 | .then() 83 | .statusCode(200) 84 | .body( 85 | "booking.firstname", 86 | equalTo("Jim"), 87 | "booking.lastname", 88 | equalTo("Brown"), 89 | "booking.totalprice", 90 | equalTo(111), 91 | "booking.depositpaid", 92 | equalTo(true), 93 | "booking.bookingdates.checkin", 94 | equalTo("2018-01-01"), 95 | "booking.bookingdates.checkout", 96 | equalTo("2019-01-01")) 97 | .extract() 98 | .response() 99 | .prettyPrint(); 100 | } 101 | 102 | @Test 103 | // Given_When_Then 104 | public void assertThatUserCanNotCreateABookingWithMissingMandatoryDetails() { 105 | String jsonBody = 106 | "{\n" 107 | + " \"firstname\": \"Jim\",\n" 108 | + " \"lastname\": \"Brown\",\n" 109 | + " \"depositpaid\": true,\n" 110 | + " \"bookingdates\": {\n" 111 | + " \"checkin\": \"2018-01-01\",\n" 112 | + " \"checkout\": \"2019-01-01\"\n" 113 | + " }\n" 114 | + "}"; 115 | 116 | given() 117 | .header("Content-Type", "application/json") 118 | .header("Accept", "application/json") 119 | .baseUri("https://restful-booker.herokuapp.com") 120 | .body(jsonBody) 121 | .when() 122 | .post("/booking") 123 | .then() 124 | .statusCode(500) 125 | .extract() 126 | .response() 127 | .prettyPrint(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/asserts/ValidateDB.java: -------------------------------------------------------------------------------- 1 | package asserts; 2 | 3 | import static org.junit.jupiter.api.Assertions.fail; 4 | 5 | import com.typesafe.config.Config; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.concurrent.TimeUnit; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.assertj.core.api.Assertions; 12 | import org.assertj.core.api.SoftAssertions; 13 | import org.awaitility.Awaitility; 14 | import org.powertester.config.TestConfig; 15 | import org.powertester.data.TestData; 16 | import org.powertester.database.DBConnection; 17 | 18 | @Slf4j 19 | public class ValidateDB { 20 | private static final Config CONFIG = TestConfig.getInstance().getConfig(); 21 | private static final String AWAITILITY_TIMEOUT_IN_SECONDS = 22 | CONFIG.getString("AWAITILITY_TIMEOUT_IN_SECONDS"); 23 | private static final String AWAITILITY_POLL_INTERVAL_IN_SECONDS = 24 | CONFIG.getString("AWAITILITY_POLL_INTERVAL_IN_SECONDS"); 25 | private static final String AWAITILITY_POLL_DELAY_IN_SECONDS = 26 | CONFIG.getString("AWAITILITY_POLL_DELAY_IN_SECONDS"); 27 | private List> resultList; 28 | private final String sqlStatement; 29 | private final String[] sqlParameters; 30 | private final TestData testData; 31 | private final SoftAssertions softly = new SoftAssertions(); 32 | 33 | private ValidateDB(TestData testData, String sqlStatement, String... sqlParameters) { 34 | this.testData = testData; 35 | this.sqlStatement = sqlStatement; 36 | this.sqlParameters = sqlParameters; 37 | } 38 | 39 | public ValidateDB with(TestData testData, String sqlStatement, String... sqlParameters) { 40 | return new ValidateDB(testData, sqlStatement, sqlParameters); 41 | } 42 | 43 | public ValidateDB hasRecordCount(int expectedCount) { 44 | try { 45 | Awaitility.await() 46 | .atMost(Long.parseLong(AWAITILITY_TIMEOUT_IN_SECONDS), TimeUnit.SECONDS) 47 | .pollInterval(Long.parseLong(AWAITILITY_POLL_INTERVAL_IN_SECONDS), TimeUnit.SECONDS) 48 | .pollDelay(Long.parseLong(AWAITILITY_POLL_DELAY_IN_SECONDS), TimeUnit.SECONDS) 49 | .alias(testData.getValue("TEST_NAME")) 50 | .untilAsserted( 51 | () -> { 52 | resultList = 53 | DBConnection.getInstance() 54 | .executePreparedStatement(sqlStatement, sqlParameters); 55 | Assertions.assertThat(resultList) 56 | .as(testData.getValue("TEST_NAME") + "has record count") 57 | .hasSize(expectedCount); 58 | }); 59 | } catch (Exception e) { 60 | fail( 61 | "Error executing SQL statement: " 62 | + sqlStatement 63 | + " with parameters: " 64 | + String.join(", ", sqlParameters), 65 | e); 66 | } 67 | 68 | return this; 69 | } 70 | 71 | public ValidateDB andActualValuesMatchExpectedValues() { 72 | log.info("Validating actual values from DB with expected values from test data"); 73 | 74 | Map actualValues = resultList.get(0); 75 | testData.getKeyTypeMap().entrySet().stream() 76 | .filter(entry -> entry.getValue().equalsIgnoreCase("output")) 77 | .forEach( 78 | entry -> { 79 | String key = entry.getKey(); 80 | String expectedValue = testData.getKeyValueMap().get(key); 81 | String actualValue = actualValues.get(key); 82 | softly 83 | .assertThat(actualValue) 84 | .as( 85 | testData.getValue("TEST_NAME") 86 | + " with sqlParameters: " 87 | + Arrays.toString(sqlParameters) 88 | + ": key: " 89 | + key) 90 | .isEqualTo(expectedValue); 91 | }); 92 | 93 | return this; 94 | } 95 | 96 | public void assertAll() { 97 | softly.assertAll(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/asserts/VerifyResponse.java: -------------------------------------------------------------------------------- 1 | package asserts; 2 | 3 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 4 | 5 | import io.restassured.response.Response; 6 | import org.assertj.core.api.Assertions; 7 | import org.assertj.core.api.SoftAssertions; 8 | 9 | public abstract class VerifyResponse> { 10 | protected SELF_TYPE selfType; 11 | protected Response response; 12 | protected SoftAssertions softAssertions; 13 | 14 | protected VerifyResponse(Class selfType, Response response) { 15 | this.selfType = selfType.cast(this); 16 | this.response = response; 17 | this.softAssertions = new SoftAssertions(); 18 | } 19 | 20 | public SELF_TYPE statusCodeIs(int statusCode) { 21 | Assertions.assertThat(response.getStatusCode()).describedAs("statusCode").isEqualTo(statusCode); 22 | 23 | return selfType; 24 | } 25 | 26 | public SELF_TYPE containsValue(String value) { 27 | softAssertions 28 | .assertThat(response.getBody().asString()) 29 | .describedAs("responseBody") 30 | .contains(value); 31 | 32 | return selfType; 33 | } 34 | 35 | public SELF_TYPE matchesSchema(String fileClassPath) { 36 | softAssertions 37 | .assertThat(response.then().body(matchesJsonSchemaInClasspath(fileClassPath))) 38 | .describedAs("Schema validation") 39 | .getWritableAssertionInfo(); 40 | 41 | return selfType; 42 | } 43 | 44 | public void assertAll() { 45 | softAssertions.assertAll(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/booking/BookingTests.java: -------------------------------------------------------------------------------- 1 | package booking; 2 | 3 | import static org.apache.http.HttpStatus.*; 4 | import static org.powertester.auth.Scope.ADMIN; 5 | import static org.powertester.auth.Scope.GUEST; 6 | 7 | import io.restassured.response.Response; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.junit.jupiter.api.AfterEach; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Nested; 12 | import org.junit.jupiter.api.Test; 13 | import org.powertester.annotations.FailingTest; 14 | import org.powertester.annotations.RegressionTest; 15 | import org.powertester.booking.Booking; 16 | import org.powertester.booking.BookingAPI; 17 | 18 | @Slf4j 19 | @RegressionTest 20 | public class BookingTests { 21 | public static final String READ_UPDATE_BOOKING_SCHEMA_FILE_PATH = 22 | "schemas/read-update-booking-schema.json"; 23 | public static final String CREATE_BOOKING_SCHEMA_FILE_PATH = "schemas/create-booking-schema.json"; 24 | private Long bookingId; 25 | private Booking booking; 26 | 27 | // Setup: Create a booking 28 | @BeforeEach 29 | public void setup() { 30 | // Arrange 31 | booking = Booking.getInstance(); 32 | // Act 33 | Response response = BookingAPI.useAs(ADMIN).newBooking(booking); 34 | 35 | // Assert 36 | VerifyBookingResponse.assertThat(response) 37 | .statusCodeIs(SC_OK) 38 | .matchesSchema(CREATE_BOOKING_SCHEMA_FILE_PATH) 39 | .postHasBooking(booking) 40 | .assertAll(); 41 | 42 | // Set bookingId 43 | bookingId = response.body().jsonPath().getLong("bookingid"); 44 | } 45 | 46 | // TearDown: Delete the booking 47 | @AfterEach 48 | public void tearDown() { 49 | Response response = BookingAPI.useAs(ADMIN).deleteBooking(bookingId); 50 | 51 | // Assert (It should ideally be 200 but this application has a bug and it gives 201) 52 | VerifyBookingResponse.assertThat(response).statusCodeIs(SC_CREATED).assertAll(); 53 | } 54 | 55 | @Nested 56 | class AdminUser { 57 | @Test 58 | void assertThatAUserCanGetAnExistingBooking() { 59 | // Act 60 | Response response = BookingAPI.useAs(GUEST).getBooking(bookingId); 61 | 62 | // Assert 63 | VerifyBookingResponse.assertThat(response) 64 | .statusCodeIs(SC_OK) 65 | .matchesSchema(READ_UPDATE_BOOKING_SCHEMA_FILE_PATH) 66 | .hasBooking(booking) 67 | .assertAll(); 68 | } 69 | 70 | @Test 71 | void assertThatAUserCanUpdateAnExistingBooking() { 72 | // Arrange 73 | booking.setFirstname("Vinod"); 74 | 75 | // Act 76 | Response response = BookingAPI.useAs(ADMIN).updateBooking(booking, bookingId); 77 | 78 | // Assert 79 | VerifyBookingResponse.assertThat(response) 80 | .statusCodeIs(SC_OK) 81 | .matchesSchema("schemas/read-update-booking-schema.json") 82 | .hasBooking(booking) 83 | .assertAll(); 84 | } 85 | 86 | @FailingTest 87 | @Test 88 | void assertThatAUserCanPartiallyUpdateAnExistingBooking() { 89 | // Arrange 90 | Booking partialBooking = 91 | Booking.builder().setFirstname("Pramod").setLastname("Yadav").build(); 92 | 93 | log.info("partialBookingBody: {}", partialBooking); 94 | 95 | // Act 96 | Response response = BookingAPI.useAs(ADMIN).patchBooking(partialBooking, bookingId); 97 | 98 | // Assert 99 | Booking expectedBooking = 100 | booking 101 | .setFirstname(partialBooking.getFirstname()) 102 | .setLastname(partialBooking.getLastname()); 103 | 104 | VerifyBookingResponse.assertThat(response) 105 | .statusCodeIs(SC_OK) 106 | .matchesSchema("schemas/read-update-booking-schema.json") 107 | .hasBooking(expectedBooking) 108 | .assertAll(); 109 | } 110 | } 111 | 112 | @Nested 113 | class GuestUser { 114 | @Test 115 | void assertThatAUserCanNotUpdateAnExistingBookingWithoutValidAuthentication() { 116 | // Arrange 117 | booking.setFirstname("Vinod"); 118 | 119 | // Act 120 | Response response = BookingAPI.useAs(GUEST).updateBooking(booking, bookingId); 121 | 122 | // Assert 123 | VerifyBookingResponse.assertThat(response).statusCodeIs(SC_FORBIDDEN).assertAll(); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/booking/VerifyBookingResponse.java: -------------------------------------------------------------------------------- 1 | package booking; 2 | 3 | import asserts.VerifyResponse; 4 | import io.restassured.response.Response; 5 | import org.powertester.booking.Booking; 6 | import org.powertester.booking.BookingResponse; 7 | 8 | public class VerifyBookingResponse extends VerifyResponse { 9 | private VerifyBookingResponse(Response response) { 10 | super(VerifyBookingResponse.class, response); 11 | } 12 | 13 | public static VerifyBookingResponse assertThat(Response response) { 14 | return new VerifyBookingResponse(response); 15 | } 16 | 17 | public VerifyBookingResponse postHasBooking(Booking expectedBooking) { 18 | BookingResponse bookingResponse = 19 | response.then().extract().response().as(BookingResponse.class); 20 | 21 | softAssertions 22 | .assertThat(bookingResponse.getBooking()) 23 | .describedAs("booking") 24 | .isEqualTo(expectedBooking); 25 | 26 | return this; 27 | } 28 | 29 | public VerifyBookingResponse hasBooking(Booking expectedBooking) { 30 | Booking bookingResponse = response.then().extract().response().as(Booking.class); 31 | 32 | softAssertions.assertThat(bookingResponse).describedAs("booking").isEqualTo(expectedBooking); 33 | 34 | return this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/booking/explore-booking-api.http: -------------------------------------------------------------------------------- 1 | ### Create auth token 2 | # curl -X POST 3 | # https://restful-booker.herokuapp.com/auth 4 | # -H 'Content-Type: application/json' 5 | # -d '{ 6 | # "username" : "admin", 7 | # "password" : "password123" 8 | #}' 9 | POST {{host}}/auth 10 | Content-Type: application/json 11 | 12 | { 13 | "username": "{{username}}", 14 | "password": "{{password}}" 15 | } 16 | 17 | > {% 18 | client.global.set("auth_token", response.body.token); 19 | client.test("Request executed successfully", function () { 20 | client.assert(response.status === 200, "Response status is not 200"); 21 | }); 22 | %} 23 | 24 | ### Get all bookings 25 | # curl -i https://restful-booker.herokuapp.com/booking 26 | GET {{host}}/booking 27 | 28 | ### Create a booking 29 | # curl -X POST 30 | # https://restful-booker.herokuapp.com/booking 31 | # -H 'Content-Type: application/json' 32 | # -d '{ 33 | # "firstname" : "Jim", 34 | # "lastname" : "Brown", 35 | # "totalprice" : 111, 36 | # "depositpaid" : true, 37 | # "bookingdates" : { 38 | # "checkin" : "2018-01-01", 39 | # "checkout" : "2019-01-01" 40 | # }, 41 | # "additionalneeds" : "Breakfast" 42 | #}' 43 | POST {{host}}/booking 44 | Content-Type: application/json 45 | Accept: application/json 46 | 47 | { 48 | "firstname": "Jim", 49 | "lastname": "Brown", 50 | "totalprice": 111, 51 | "depositpaid": true, 52 | "bookingdates": { 53 | "checkin": "2018-01-01", 54 | "checkout": "2019-01-01" 55 | }, 56 | "additionalneeds": "Breakfast" 57 | } 58 | 59 | > {% 60 | client.global.set("booking_id", response.body.bookingid); 61 | client.test("Request executed successfully", function () { 62 | client.assert(response.status === 200, "Response status is not 200"); 63 | var firstname = response.body.booking.firstname; 64 | client.log("firstname: " + firstname) 65 | client.assert(firstname === "Jim", "Expected booking firstname: Jim, Actual booking firstname : " + firstname); 66 | }); 67 | %} 68 | 69 | ### Get a particular booking 70 | # curl -i https://restful-booker.herokuapp.com/booking/1 71 | GET {{host}}/booking/{{booking_id}} 72 | Accept: application/json 73 | 74 | > {% 75 | client.test("Request executed successfully", function () { 76 | client.assert(response.status === 200, "Response status is not 200"); 77 | }); 78 | %} 79 | 80 | ### Update an existing booking 81 | # curl -X PUT 82 | # https://restful-booker.herokuapp.com/booking/1 83 | # -H 'Content-Type: application/json' 84 | # -H 'Accept: application/json' 85 | # -H 'Cookie: token=abc123' 86 | # -d '{ 87 | # "firstname" : "James", 88 | # "lastname" : "Brown", 89 | # "totalprice" : 111, 90 | # "depositpaid" : true, 91 | # "bookingdates" : { 92 | # "checkin" : "2018-01-01", 93 | # "checkout" : "2019-01-01" 94 | # }, 95 | # "additionalneeds" : "Breakfast" 96 | #}' 97 | PUT {{host}}/booking/{{booking_id}} 98 | Accept: application/json 99 | Cookie: token={{auth_token}} 100 | Content-Type: application/json 101 | 102 | { 103 | "firstname": "Pramod", 104 | "lastname": "Yadav", 105 | "totalprice": 111, 106 | "depositpaid": true, 107 | "bookingdates": { 108 | "checkin": "2018-01-01", 109 | "checkout": "2019-01-01" 110 | }, 111 | "additionalneeds": "Breakfast" 112 | } 113 | 114 | > {% 115 | client.test("Request executed successfully", function () { 116 | client.assert(response.status === 200, "Response status is not 200"); 117 | }); 118 | %} 119 | 120 | ### Partial update a booking 121 | # curl -X PATCH 122 | # https://restful-booker.herokuapp.com/booking/1 123 | # -H 'Content-Type: application/json' 124 | # -H 'Accept: application/json' 125 | # -H 'Cookie: token=abc123' 126 | # -d '{ 127 | # "firstname" : "James", 128 | # "lastname" : "Brown" 129 | #}' 130 | PATCH {{host}}/booking/{{booking_id}} 131 | Accept: application/json 132 | Cookie: token={{auth_token}} 133 | Content-Type: application/json 134 | 135 | { 136 | "firstname": "Vinod", 137 | "lastname": "kumar" 138 | } 139 | 140 | > {% 141 | client.test("Request executed successfully", function () { 142 | client.assert(response.status === 200, "Response status is not 200"); 143 | }); 144 | %} 145 | 146 | ### Delete booking 147 | # curl -X DELETE 148 | # https://restful-booker.herokuapp.com/booking/1 149 | # -H 'Content-Type: application/json' 150 | # -H 'Cookie: token=abc123' 151 | DELETE {{host}}/booking/{{booking_id}} 152 | Cookie: token={{auth_token}} 153 | Content-Type: application/json 154 | 155 | > {% 156 | client.test("Request executed successfully", function () { 157 | var status = response.status; 158 | client.log("response.status: " + status); 159 | client.assert(status === 200, "Expected response: 200; Actual response: " + status); 160 | }); 161 | %} 162 | 163 | ### Get a particular booking 164 | # curl -i https://restful-booker.herokuapp.com/booking/1 165 | GET {{host}}/booking/{{booking_id}} 166 | Accept: application/json 167 | 168 | > {% 169 | client.test("Request executed successfully", function () { 170 | var status = response.status; 171 | client.log("response.status: " + status); 172 | client.assert(status === 404, "Expected response: 404; Actual response: " + status); 173 | }); 174 | %} 175 | 176 | ### health check endpoint 177 | # curl -i https://restful-booker.herokuapp.com/ping 178 | GET {{host}}/ping 179 | 180 | > {% 181 | client.test("Request executed successfully", function () { 182 | client.assert(response.status === 201, "Response status is not 200"); 183 | }); 184 | %} 185 | 186 | ### 187 | -------------------------------------------------------------------------------- /src/test/java/env/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "localhost": { 3 | "host": "https://localhost", 4 | "username": "to-be-overridden-from-http-client.private.env.json", 5 | "password": "to-be-overridden-from-http-client.private.env.json" 6 | }, 7 | "develop": { 8 | "host": "https://restful-booker.herokuapp.com", 9 | "username": "to-be-overridden-from-http-client.private.env.json", 10 | "password": "to-be-overridden-from-http-client.private.env.json" 11 | }, 12 | "staging": { 13 | "host": "https://staging-restful-booker.herokuapp.com", 14 | "username": "to-be-overridden-from-http-client.private.env.json", 15 | "password": "to-be-overridden-from-http-client.private.env.json" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/healthcheck/HealthCheckTests.java: -------------------------------------------------------------------------------- 1 | package healthcheck; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import io.restassured.response.Response; 6 | import org.junit.jupiter.api.Test; 7 | import org.powertester.annotations.HealthCheckTest; 8 | import org.powertester.healthcheck.HealthCheckAPI; 9 | 10 | @HealthCheckTest 11 | class HealthCheckTests { 12 | @Test 13 | void assertThatRestfulBookerApplicationIsUpAndHealthy() { 14 | Response response = HealthCheckAPI.healthCheck(); 15 | assertEquals(201, response.getStatusCode()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/unittests/CSVUnitTests.java: -------------------------------------------------------------------------------- 1 | package unittests; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.junit.jupiter.params.provider.CsvFileSource; 5 | import org.powertester.annotations.CsvTest; 6 | import org.powertester.annotations.UnitTest; 7 | 8 | @UnitTest 9 | @Slf4j 10 | public class CSVUnitTests { 11 | 12 | @CsvTest 13 | @CsvFileSource(files = "src/test/resources/testdata/data.csv", numLinesToSkip = 1) 14 | void testWithCsvFileSource(String testName, String userID, String access) { 15 | log.info("userID: " + userID + ", access: " + access); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/testdata/data.csv: -------------------------------------------------------------------------------- 1 | testName, userId, Access 2 | Guest User, Guest, Read Only 3 | Admin User, Admin, Create/Read/Update/Delete 4 | Maintenance User, Maintainer, Read/Update 5 | --------------------------------------------------------------------------------