diff --git a/jdk_21_maven/cs/rest/flight-search-api/.gitattributes b/jdk_21_maven/cs/rest/flight-search-api/.gitattributes new file mode 100644 index 000000000..3b41682ac --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/jdk_21_maven/cs/rest/flight-search-api/.gitignore b/jdk_21_maven/cs/rest/flight-search-api/.gitignore new file mode 100644 index 000000000..d5af496f1 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/.gitignore @@ -0,0 +1,36 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Environment file +.env diff --git a/jdk_21_maven/cs/rest/flight-search-api/CODEOWNERS b/jdk_21_maven/cs/rest/flight-search-api/CODEOWNERS new file mode 100644 index 000000000..de537767b --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/CODEOWNERS @@ -0,0 +1,2 @@ +# These owners will be the default owners for everything in the repo. +* @Rapter1990 @furkantarikgocmen \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/CODE_OF_CONDUCT.md b/jdk_21_maven/cs/rest/flight-search-api/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..d43f9f233 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/CONTRIBUTING.md b/jdk_21_maven/cs/rest/flight-search-api/CONTRIBUTING.md new file mode 100644 index 000000000..c1807d793 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/CONTRIBUTING.md @@ -0,0 +1,182 @@ +# Contributing Guidelines + +Thank you for considering contributing to the "afet-yonetim-sistemi" project! We appreciate your interest and welcome +any contributions that can help us improve the project. + +Please take a moment to review these guidelines before making any contributions. Following these guidelines will help +you maintain a collaborative and inclusive environment for everyone involved. + +## Table of Contents + +- [How to Contribute](#how-to-contribute) +- [Reporting Issues](#reporting-issues) +- [Submitting Changes](#submitting-changes) +- [Code Style](#code-style) +- [Commit Messages](#commit-messages) +- [Testing](#testing) +- [Documentation](#documentation) +- [Community Guidelines](#community-guidelines) + +## How to Contribute + +Thank you for considering contributing to our project! To make the process smooth, please follow these steps: + +1. **Fork the Repository:** + + - Visit our [GitHub repository](https://github.com/EmreKiziltoprak/CRM_experiment) and fork it to your GitHub account. + +2. **Create a New Branch:** + + - Create a branch for your contribution by referring to + the [Naming Conventions of Branches](#branch-naming-conventions) for guidance. + - Ensure your feature branch is associated with the corresponding GitHub issue from the 'Development' section in + each issue page. + +3. **Make Changes:** + + - Implement your changes in the branch you created. + - Thoroughly test your changes to ensure they work as expected. + +4. **Commit Changes:** + + - Commit your changes with clear and concise messages. Refer to the [Commit Messages](#commit-messages) section for + guidance. + +5. **Push Changes:** + + - Push your changes to your forked repository. + +6. **Open a Pull Request (PR):** + + - Open a PR against the `main` branch of the original repository by referring to + the [Pull Request Naming Conventions](#pull-request-naming-conventions) for guidance. + - Provide a detailed description of your changes in the PR. + - Link relevant issues or discussions in the PR description. + - Follow the pull request naming conventions outlined below. + - PR title should contain the issue/feature/bug number. + +7. **Review and Feedback:** + - Once your PR is submitted, project maintainers will review it and provide feedback. + - Be responsive to feedback and make necessary changes. + +## Development Standards + +Our project adheres to the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) for +collaboration. +Issues are managed on GitHub Discussions, with plans to transition them to Github. + +### Branch Naming Conventions: + +- `bugfix/{github-issue-number}/{optional-description}` +- `development/{github-issue-number}/{optional-description}` +- `refactor/{github-issue-number}/{optional-description}` +- `hotfix/{github-issue-number}/{optional-description}` + +> Example : `development/1/add-feature-to-handle-user-authentication` + +### Pull Request Naming Conventions: + +- `#{github-issue-number} | {header-for-summary-of-development}` + +> Example : `#PR 1 | Add feature to handle user authentication` + +Here are some additional standards to keep in mind: + +- Link your feature branch to the corresponding GitHub issue. +- `main` branch is protected. +- PR should be linked to the relevant issue within the GitHub Project. +- Only one code owner's approval is needed for merging feature branches. +- PR should be squash-merged to avoid merge commit history. +- PR should pass build and necessary tests before merging. +- Releases should be tagged with a version. +- Commit messages should be subjectless. +- Resolve comments within the PR by the commenter. +- PR should be concise and address one thing. +- Avoid including secret/credential information. +- Code should comply with existing coding standards; no new standards should be introduced without justification. + +## Reporting Issues + +If you encounter any bugs, have feature requests, or want to provide general feedback, please feel free to open a +discussion +in the "CRM_experiment" repository. When creating a discussion, please provide as much detail as possible, +including: + +- A clear and descriptive title +- Steps to reproduce the issue +- Expected behavior +- Actual behavior +- Any error messages or logs + +## Submitting Changes + +When submitting changes, please ensure the following: + +- Your code adheres to the project's [code style guidelines](#code-style). +- You have added appropriate tests for your changes (see [Testing](#testing)). +- The documentation has been updated, if necessary (see [Documentation](#documentation)). +- Your commits are clean and have clear commit messages (see [Commit Messages](#commit-messages)). + +## Code Style + +To maintain a consistent codebase, please follow the existing code style used in the project. If necessary, refer to any +code style guidelines mentioned in the repository. + +## Commit Messages + +Writing clear and descriptive commit messages is essential for the maintainability of the project. Follow these +guidelines when writing commit messages: + +- Limit the subject line to 50 characters. +- Begin the subject line with a verb in the present tense (e.g., "Fix," "Add," "Update"). +- Use the imperative mood (e.g., "Fix bug" instead of "Fixed bug"). +- Provide additional details in the commit message body if necessary. Explain why the change was made and any relevant + information for reviewers. + +Example: + +``` +Add feature to handle user authentication + +- Implemented a new module for user authentication +- Updated existing login page UI +- Added new API endpoint for user registration +``` + +## Testing + +If you are contributing code changes, it is important to include appropriate tests to ensure the stability and +functionality of the project. Follow these guidelines when writing tests: + +- Write tests that cover the new code you have added or modified. +- Ensure your tests are easy to understand and maintain. +- Run the existing test suite before submitting your changes to make sure they haven't introduced any regressions. +- Provide instructions on how to run the tests if they require additional setup or specific commands. + +## Documentation + +Clear and up-to-date documentation is crucial for the project's users and contributors. If your contribution introduces +new features, modifies existing functionality, or requires changes to the documentation, please make the necessary +updates. + +Follow these guidelines for documenting your changes: + +- Update the relevant documentation files to reflect your changes. +- Write clear and concise explanations. +- Use code examples or screenshots, if appropriate, to enhance understanding. +- Proofread your documentation to ensure it is accurate and free of errors. + +## Community Guidelines + +In addition to the technical aspects, it is important to follow the community guidelines to maintain a positive and +inclusive environment for all contributors. + +- Be respectful and considerate of others' opinions and ideas. +- Be open to feedback and willing to collaborate with other contributors. +- Avoid personal attacks, offensive language, and harassment. +- If you notice any inappropriate behavior, report it to the project maintainers. + +By adhering to these guidelines, we can work together to build a welcoming and productive community. + +Thank you for your interest in contributing to the "CRM_experiment" project! We appreciate your support and look +forward to your contributions. \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/Dockerfile b/jdk_21_maven/cs/rest/flight-search-api/Dockerfile new file mode 100644 index 000000000..3628f4f70 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/Dockerfile @@ -0,0 +1,27 @@ +# Stage 1: Build stage +FROM maven:3.9.7-amazoncorretto-21 AS build + +# Copy Maven files for dependency resolution +COPY pom.xml ./ +COPY .mvn .mvn + +# Copy application source code +COPY src src + +# Build the project and create the executable JAR +RUN mvn clean install -DskipTests + +# Stage 2: Run stage +FROM amazoncorretto:21 + +# Set working directory +WORKDIR flightsearchapi + +# Copy the JAR file from the build stage +COPY --from=build target/*.jar flightsearchapi.jar + +# Expose port 1133 +EXPOSE 1133 + +# Set the entrypoint command for running the application +ENTRYPOINT ["java", "-jar", "flightsearchapi.jar"] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/Jenkinsfile b/jdk_21_maven/cs/rest/flight-search-api/Jenkinsfile new file mode 100644 index 000000000..ad672b403 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/Jenkinsfile @@ -0,0 +1,71 @@ +pipeline { + agent any + + environment { + GIT_REPO_URL = 'https://github.com/Rapter1990/flightsearchapi.git' + BRANCH_NAME = 'main' + DOCKERHUB_USERNAME = 'noyandocker' + DOCKER_IMAGE_NAME = 'flightsearchapi-jenkins' + } + + stages { + stage('Checkout') { + steps { + script { + checkout([ + $class: 'GitSCM', + branches: [[name: "*/${env.BRANCH_NAME}"]], + userRemoteConfigs: [[url: "${env.GIT_REPO_URL}"]] + ]) + } + } + } + + stage('Build') { + agent { + docker { + image 'maven:3.9.9-amazoncorretto-21-alpine' + } + } + steps { + sh 'mvn clean install' + } + } + + stage('Build Docker Image') { + agent { + docker { + image 'docker:27.5.1' + } + } + steps { + sh "docker build -t ${env.DOCKERHUB_USERNAME}/${env.DOCKER_IMAGE_NAME}:latest ." + } + } + + stage('Push Docker Image') { + agent { + docker { + image 'docker:27.5.1' + } + } + steps { + withDockerRegistry([credentialsId: 'docker-hub-credentials', url: '']) { + sh "docker push ${env.DOCKERHUB_USERNAME}/${env.DOCKER_IMAGE_NAME}:latest" + } + } + } + + } + + post { + always { + cleanWs(cleanWhenNotBuilt: false, + deleteDirs: true, + disableDeferredWipeout: true, + notFailBuild: true, + patterns: [[pattern: '.gitignore', type: 'INCLUDE'], + [pattern: '.propsfile', type: 'EXCLUDE']]) + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/LICENSE.md b/jdk_21_maven/cs/rest/flight-search-api/LICENSE.md new file mode 100644 index 000000000..b208ddd05 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/LICENSE.md @@ -0,0 +1,30 @@ +MIT License +=========== + +This project is licensed under the terms of the MIT license. + +MIT License +----------- + +SPDX short identifier: MIT + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + Software. + +2. The Software is provided "as is," without warranty of any kind, express or implied, including but not limited to the + warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors + or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or + otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software. + +3. Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or + otherwise to promote the sale, use or other dealings in this Software without prior written authorization. + +4. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software + developed by the project team (https://github.com/Rapter1990/flightsearchapi)." + +5. Redistributions of any form whatsoever must also include a copy of this license document in the distribution. \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/PULL_REQUEST_TEMPLATE.md b/jdk_21_maven/cs/rest/flight-search-api/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..b33f7cb69 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,49 @@ +### Description + +Please provide a brief description of the changes made in this pull request and the issue(s) it addresses. + +....Write Description.... + +#### Related Issue + +Link to the issue this pull request addresses: #[issue number] + +### Checklist + +Before submitting your pull request, ensure the following: + +- [ ] **Title and Branch Naming Conventions:** + + - The pull request title follows the + standard: [Pull Request Naming Conventions](CONTRIBUTING.md#pull-request-naming-conventions). + - The branch name follows one of the conventions outlined in + the [Branch Naming Conventions](CONTRIBUTING.md#branch-naming-conventions). + +- [ ] **Local Testing:** + + - I have tested my changes locally on Postman, and they are working as expected. + +- [ ] **Code Quality:** + + - The code is formatted according to the project's coding guidelines and style. + - The code has been reviewed to ensure its quality. + - The code does not contain any issues flagged by SonarLint. + +- [ ] **Documentation:** + + - Necessary documentation has been added or existing documentation has been updated, specifically detailing changes + made in Postman. + +- [ ] **Testing:** + + - Relevant unit tests have been written and included. + - Relevant integration tests have been written and included. + +- [ ] **Reviewers and Assignees:** + + - Default reviewers have been assigned to this pull request. + - Assignees have been added if necessary. + +- [ ] **Labels and Associations:** + - The pull request is associated with its related issue. + - Appropriate labels have been applied. \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/README.md b/jdk_21_maven/cs/rest/flight-search-api/README.md new file mode 100644 index 000000000..db8f71371 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/README.md @@ -0,0 +1,476 @@ +# Case Study - Flight Search Api + +

+ Main Information +

+ +### 📖 Information + + + + +### Explore Rest APIs + +Endpoints Summary + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodUrlDescriptionRequest BodyPath VariableResponse
POST/api/v1/authentication/user/registerRegister for Admin or UserRegisterRequestCustomResponse<Void>
POST/api/v1/authentication/user/loginLogin for Admin or UserLoginRequestCustomResponse<TokenResponse>
POST/api/v1/authentication/user/refresh-tokenRefresh Token for Admin or UserTokenRefreshRequestCustomResponse<TokenResponse>
POST/api/v1/authentication/user/logoutLogout for Admin or UserTokenInvalidateRequestCustomResponse<Void>
POST/api/v1/airportsCreate a new AirportCreateAirportRequestCustomResponse<String>
GET/api/v1/airportsGet all AirportsAirportPagingRequestCustomResponse<CustomPagingResponse<AirportResponse>>
GET/api/v1/airports/{id}Get Airport by IDUUIDCustomResponse<AirportResponse>
PUT/api/v1/airports/{id}Update an AirportUpdateAirportRequestUUIDCustomResponse<AirportResponse>
DELETE/api/v1/airports/{id}Delete an AirportUUIDCustomResponse<String>
POST/api/v1/flightsCreate a new FlightCreateFlightRequestCustomResponse<String>
GET/api/v1/flightsGet all FlightsAirportPagingRequestCustomResponse<CustomPagingResponse<FlightResponse>>
GET/api/v1/flights/{id}Get Flight by IDUUIDCustomResponse<FlightResponse>
PUT/api/v1/flights/{id}Update a FlightUpdateFlightRequestUUIDCustomResponse<FlightResponse>
DELETE/api/v1/flights/{id}Delete a FlightUUIDCustomResponse<String>
POST/api/v1/flights/searchSearch for FlightsSearchFlightRequestCustomResponse<CustomPagingResponse<FlightResponse>>
+ + + +### Technologies + +--- +- Java 21 +- Spring Boot 3.0 +- Restful API +- Mapstruct +- Open Api (Swagger) +- Maven +- Junit5 +- Mockito +- Integration Tests +- Docker +- Docker Compose +- CI/CD (Github Actions - Jenkins) +- Postman +- TestContainer +- Mongo +- Prometheus +- Grafana +- Kubernetes +- JaCoCo (Test Report) + +### Postman + +``` +Import postman collection under postman_collection folder +``` + + +### Prerequisites + +#### Define Variable in .env file + +``` +MONGO_DB_NAME=flightdatabase +MONGO_DB_HOST=localhost +MONGO_DB_PORT=27017 +``` + +### Open Api (Swagger) + +``` +http://localhost:1133/swagger-ui/index.html +``` + +--- + +### JaCoCo (Test Report) + +After the command named `mvn clean install` completes, the JaCoCo report will be available at: +``` +target/site/jacoco/index.html +``` +Navigate to the `target/site/jacoco/` directory. + +Open the `index.html` file in your browser to view the detailed coverage report. + +--- + +### Maven, Docker and Kubernetes Running Process + + +### Maven Run +To build and run the application with `Maven`, please follow the directions shown below; + +```sh +$ cd flightsearchapi +$ mvn clean install +$ mvn spring-boot:run +``` + +--- + +### Docker Run +The application can be built and run by the `Docker` engine. The `Dockerfile` has multistage build, so you do not need to build and run separately. + +Please follow directions shown below in order to build and run the application with Docker Compose file; + +```sh +$ cd flightsearchapi +$ docker-compose up -d +``` + +If you change anything in the project and run it on Docker, you can also use this command shown below + +```sh +$ cd flightsearchapi +$ docker-compose up --build +``` + +To monitor the application, you can use the following tools: + +- **Prometheus**: + Open in your browser at [http://localhost:9090](http://localhost:9090) + Prometheus collects and stores application metrics. + +- **Grafana**: + Open in your browser at [http://localhost:3000](http://localhost:3000) + Grafana provides a dashboard for visualizing the metrics. + **Default credentials**: + - Username: `admin` + - Password: `admin` + +- Define prometheus data source url, use this link shown below + +``` +http://prometheus:9090 +``` + +--- + + +### Kubernetes Run +To build and run the application with `Maven`, please follow the directions shown below; + +- Start Minikube + +```sh +$ minikube start +``` + +- Open Minikube Dashboard + +```sh +$ minikube dashboard +``` + +- To deploy the application on Kubernetes, apply the Kubernetes configuration file underneath k8s folder + +```sh +$ kubectl apply -f k8s +``` + +- To open Prometheus, click tunnel url link provided by the command shown below to reach out Prometheus + +```sh +minikube service prometheus-service +``` + + +- To open Grafana, click tunnel url link provided by the command shown below to reach out Prometheus + +```sh +minikube service grafana-service +``` + +- Define prometheus data source url, use this link shown below + +``` +http://prometheus-service.default.svc.cluster.local:9090 +``` + + +--- +### Docker Image Location + +``` +https://hub.docker.com/repository/docker/noyandocker/flightsearchapi/general +https://hub.docker.com/repository/docker/noyandocker/flightsearchapi-jenkins/general +``` + +### Jenkins + +- Go to `jenkins` folder +- Run `docker-compose up -d` +- Open Jenkins in the browser via `localhost:8080` +- Go to pipeline named `flightsearchapi` +- Run Pipeline +- Show `Pipeline Step` to verify if it succeeded or failed + + +### Screenshots + +
+Click here to show the screenshots of project +

Figure 1

+ +

Figure 2

+ +

Figure 3

+ +

Figure 4

+ +

Figure 5

+ +

Figure 6

+ +

Figure 7

+ +

Figure 8

+ +

Figure 9

+ +

Figure 10

+ +

Figure 11

+ +

Figure 12

+ +

Figure 13

+ +

Figure 14

+ +

Figure 15

+ +

Figure 16

+ +

Figure 17

+ +

Figure 18

+ +

Figure 19

+ +

Figure 20

+ +

Figure 21

+ +

Figure 22

+ +

Figure 23

+ +

Figure 24

+ +
+ + +### Contributors + +- [Sercan Noyan Germiyanoğlu](https://github.com/Rapter1990) +- [Furkan Tarık Göçmen](https://github.com/furkantarikgocmen) \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/SECURITY.md b/jdk_21_maven/cs/rest/flight-search-api/SECURITY.md new file mode 100644 index 000000000..e0b7b5729 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +At FlightSearchApi , we take the security of our software and community seriously. We appreciate your efforts +to responsibly disclose any security vulnerabilities you may find. + +## Reporting a Vulnerability + +If you believe you have discovered a security vulnerability or have concerns regarding the security of our project, +please help us by disclosing it to us in a responsible manner. We encourage you to follow the guidelines outlined below. + +To report a vulnerability: + +1. Create a new issue in this repository to report the vulnerability. Please provide as much information as possible to + help us understand and reproduce the issue. +2. If you believe the vulnerability may have sensitive implications, please encrypt your findings using our PGP key ( + available in the repository). +3. We will review your report and respond to it as quickly as possible. +4. We appreciate your patience and cooperation during the disclosure process. + +## Bug Bounty Program + +At this time, FlightSearchApi does not offer a formal bug bounty program for the project. + +## Supported Versions + +Please note that only the latest release of this project is officially supported with security updates. It is highly +recommended to keep your software up-to-date with the latest version. + +## Security Considerations + +Here are some security considerations when using this project: + +1. Always ensure that you are using the latest version of the project to benefit from security updates and bug fixes. +2. Regularly update your dependencies to include any security patches that may have been released. +3. Follow best practices for secure coding and deployment, such as using strong and unique passwords, enabling + two-factor authentication, and limiting access privileges. +4. Be cautious of the data you input or share, especially when interacting with external systems or APIs. + +## Contact + +If you need to contact us regarding any security-related matters or have any questions about the security of this +project, you can reach out to us at: + +- Email: [javadevelopercasestudy@hotmail.com](mailto:javadevelopercasestudy@hotmail.com) + +We kindly request that you refrain from spam, social engineering, or any other malicious activities. We appreciate your +cooperation in keeping our community safe and secure. + +Thank you for your contributions and for helping us maintain a secure ecosystem. \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/data/prometheus/config/prometheus.yml b/jdk_21_maven/cs/rest/flight-search-api/data/prometheus/config/prometheus.yml new file mode 100644 index 000000000..c36980ffc --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/data/prometheus/config/prometheus.yml @@ -0,0 +1,23 @@ +global: + scrape_interval: 120s # By default, scrape targets every 15 seconds. + evaluation_interval: 120s # By default, scrape targets every 15 seconds. + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'Spring Boot Application input' + metrics_path: '/actuator/prometheus' + scrape_interval: 2s + static_configs: + - targets: [ 'flightsearchapi:1133' ] + labels: + application: 'Flight SSearch Api' \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/docker-compose.yml b/jdk_21_maven/cs/rest/flight-search-api/docker-compose.yml new file mode 100644 index 000000000..779ba549a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/docker-compose.yml @@ -0,0 +1,68 @@ +services: + + mongodb: + image: "mongo:latest" + container_name: mongo-container + restart: always + ports: + - "27017:27017" + volumes: + - mongodb_data_container:/data/db + networks: + - flightsearchapinetwork + + + flightsearchapi: + image: 'flightsearchapi:latest' + build: + context: . + dockerfile: Dockerfile + container_name: flightsearchapi + restart: on-failure + env_file: + - .env # Use the .env file for environment variables + ports: + - "1133:1133" + environment: + - server.port=1133 + - spring.data.mongodb.host=mongodb + - spring.data.mongodb.port=${MONGO_DB_PORT} + - spring.data.mongodb.database=${MONGO_DB_NAME} + depends_on: + - mongodb + networks: + - flightsearchapinetwork + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: unless-stopped + ports: + - "9090:9090" + volumes: + - ./data/prometheus/config:/etc/prometheus/ + command: + - '--config.file=/etc/prometheus/prometheus.yml' + networks: + - flightsearchapinetwork + + grafana: + image: "grafana/grafana-oss:latest" + pull_policy: always + container_name: grafana + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - ./data/grafana:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_SERVER_DOMAIN=localhost + networks: + - flightsearchapinetwork + +volumes: + mongodb_data_container: + +networks: + flightsearchapinetwork: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/jenkins/Dockerfile b/jdk_21_maven/cs/rest/flight-search-api/jenkins/Dockerfile new file mode 100644 index 000000000..5c4c50b11 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/jenkins/Dockerfile @@ -0,0 +1,12 @@ +FROM jenkins/jenkins:lts + +# Plugin list +COPY plugins.txt /usr/share/jenkins/ref/plugins.txt +RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt + +# For Groovy scripts, init.d directory +COPY init.groovy.d/ /var/jenkins_home/init.groovy.d/ + +# Install Docker CLI +USER root +RUN apt-get update && apt-get install -y docker.io diff --git a/jdk_21_maven/cs/rest/flight-search-api/jenkins/docker-compose.yml b/jdk_21_maven/cs/rest/flight-search-api/jenkins/docker-compose.yml new file mode 100644 index 000000000..3dc7c1cd3 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/jenkins/docker-compose.yml @@ -0,0 +1,27 @@ + +services: + jenkins: + build: + context: . + dockerfile: Dockerfile + container_name: jenkins-server + ports: + - "8080:8080" # Expose Jenkins UI on port 8080 + - "50000:50000" # Expose port for Jenkins agents + volumes: + - jenkins_home:/var/jenkins_home # Persistent Jenkins data + - /var/run/docker.sock.raw:/var/run/docker.sock # Mount Docker socket for Docker builds + - ../k8s:/var/jenkins_home/k8s # Mount Kubernetes configuration files (optional) + - ./init.groovy.d:/var/jenkins_home/init.groovy.d # Mount Jenkins init scripts (optional) + environment: + JAVA_OPTS: "-Djenkins.install.runSetupWizard=false" # Skip setup wizard (optional) + user: root # Run as root to allow installing dependencies + networks: + - common-network + +volumes: + jenkins_home: + +networks: + common-network: + driver: bridge \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/jenkins/init.groovy.d/create-pipeline.groovy b/jdk_21_maven/cs/rest/flight-search-api/jenkins/init.groovy.d/create-pipeline.groovy new file mode 100644 index 000000000..b112c0e31 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/jenkins/init.groovy.d/create-pipeline.groovy @@ -0,0 +1,32 @@ +import hudson.plugins.git.BranchSpec +import hudson.plugins.git.GitSCM +import hudson.plugins.git.UserRemoteConfig +import jenkins.model.* +import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition + +def instance = Jenkins.getInstance() + +def jobName = "flightsearchapi" +def job = instance.getItem(jobName) + +if (job != null) { + job.delete() +} + +def pipelineJob = instance.createProject(org.jenkinsci.plugins.workflow.job.WorkflowJob, jobName) +def definition = new CpsScmFlowDefinition( + new GitSCM( + [ + new UserRemoteConfig("https://github.com/Rapter1990/flightsearchapi.git", null, null, null) + ], + [new BranchSpec("*/main")], + false, Collections.emptyList(), + null, null, Collections.emptyList() + ), + "Jenkinsfile" +) +definition.setLightweight(true) +pipelineJob.setDefinition(definition) +pipelineJob.save() + +println("Pipeline job '${jobName}' has been successfully created!") \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/jenkins/plugins.txt b/jdk_21_maven/cs/rest/flight-search-api/jenkins/plugins.txt new file mode 100644 index 000000000..750208391 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/jenkins/plugins.txt @@ -0,0 +1,7 @@ +workflow-aggregator +git +job-dsl +ws-cleanup +docker-plugin +docker-workflow +docker-commons \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/app-config.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/app-config.yml new file mode 100644 index 000000000..d5f3ddd8a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/app-config.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: flightsearchapi-config + namespace: default +data: + server.port: "1133" + spring.data.mongodb.host: "mongodb" + spring.data.mongodb.port: "27017" + spring.data.mongodb.database: "flightdatabase" \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/flightsearchapi-deployment.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/flightsearchapi-deployment.yml new file mode 100644 index 000000000..381a185a8 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/flightsearchapi-deployment.yml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flightsearchapi + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: flightsearchapi + template: + metadata: + labels: + app: flightsearchapi + spec: + containers: + - name: flightsearchapi + image: noyandocker/flightsearchapi:latest + ports: + - containerPort: 1133 + envFrom: + - configMapRef: + name: flightsearchapi-config + volumeMounts: + - name: flightsearchapi-data + mountPath: /data/logs + volumes: + - name: flightsearchapi-data + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: flightsearchapi-service + namespace: default +spec: + selector: + app: flightsearchapi + ports: + - protocol: TCP + port: 1133 + targetPort: 1133 + type: NodePort diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/grafana-deployment.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/grafana-deployment.yml new file mode 100644 index 000000000..e0c982a32 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/grafana-deployment.yml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grafana +spec: + replicas: 1 + selector: + matchLabels: + app: grafana + template: + metadata: + labels: + app: grafana + spec: + containers: + - name: grafana + image: grafana/grafana-oss:latest + ports: + - containerPort: 3000 + env: + - name: GF_SECURITY_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: grafana-secret + key: GF_SECURITY_ADMIN_PASSWORD +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana-service +spec: + selector: + app: grafana + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + type: NodePort \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/grafana-secret.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/grafana-secret.yml new file mode 100644 index 000000000..4b1988f38 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/grafana-secret.yml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: grafana-secret + namespace: default +type: Opaque +data: + GF_SECURITY_ADMIN_PASSWORD: YWRtaW4= # Base64 encoded password (admin) \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/mongodb-pv.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/mongodb-pv.yml new file mode 100644 index 000000000..6423a17ca --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/mongodb-pv.yml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: mongodb-pv + namespace: default +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /data/mongodb + +--- + +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mongodb-pvc + namespace: default +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/mongodb-statefulset.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/mongodb-statefulset.yml new file mode 100644 index 000000000..197bde0c4 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/mongodb-statefulset.yml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mongodb + namespace: default +spec: + serviceName: mongodb + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: mongo:latest + ports: + - containerPort: 27017 + volumeMounts: + - name: mongodb-data + mountPath: /data/db + volumeClaimTemplates: + - metadata: + name: mongodb-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 5Gi + +--- + +apiVersion: v1 +kind: Service +metadata: + name: mongodb + namespace: default + labels: + app: mongodb +spec: + ports: + - name: mongo + port: 27017 + targetPort: 27017 + clusterIP: None # Headless service for StatefulSet + selector: + app: mongodb \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/prometheus-configmap.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/prometheus-configmap.yml new file mode 100644 index 000000000..9f51b9aea --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/prometheus-configmap.yml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config + namespace: default +data: + prometheus.yml: | + global: + scrape_interval: 120s # By default, scrape targets every 15 seconds. + evaluation_interval: 120s # By default, evaluate rules every 15 seconds. + + # A scrape configuration containing exactly one endpoint to scrape: + # Here it's Prometheus itself. + scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'Spring Boot Application input' + metrics_path: '/actuator/prometheus' + scrape_interval: 2s + static_configs: + - targets: ['flightsearchapi-service.default.svc.cluster.local:1133'] + labels: + application: 'Flight Search Api' \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/k8s/prometheus-deployment.yml b/jdk_21_maven/cs/rest/flight-search-api/k8s/prometheus-deployment.yml new file mode 100644 index 000000000..93696a821 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/k8s/prometheus-deployment.yml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:latest + args: + - '--config.file=/etc/prometheus/prometheus.yml' + ports: + - containerPort: 9090 + volumeMounts: + - name: prometheus-config + mountPath: /etc/prometheus/prometheus.yml + subPath: prometheus.yml # Ensure only the file is mounted + volumes: + - name: prometheus-config + configMap: + name: prometheus-config +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus-service +spec: + selector: + app: prometheus + ports: + - protocol: TCP + port: 9090 + targetPort: 9090 + type: NodePort \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/mvnw b/jdk_21_maven/cs/rest/flight-search-api/mvnw new file mode 100644 index 000000000..19529ddf8 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/jdk_21_maven/cs/rest/flight-search-api/mvnw.cmd b/jdk_21_maven/cs/rest/flight-search-api/mvnw.cmd new file mode 100644 index 000000000..249bdf382 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/jdk_21_maven/cs/rest/flight-search-api/pom.xml b/jdk_21_maven/cs/rest/flight-search-api/pom.xml new file mode 100644 index 000000000..2ba7efd47 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/pom.xml @@ -0,0 +1,286 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.1 + + + com.case + flightsearchapi + 0.0.1-SNAPSHOT + flightsearchapi + Flight Search Api Example + + + + + + + + + + + + + + + 21 + 1.77 + 0.12.3 + 1.5.5.Final + 0.2.0 + 2.1.0 + 2.1.0 + 0.8.12 + 3.5.2 + 3.5.2 + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + + + + + + org.apache.commons + commons-lang3 + 3.14.0 + + + + + org.springframework.boot + spring-boot-starter-security + + + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + + io.jsonwebtoken + jjwt-api + ${jsonwebtoken.version} + + + + io.jsonwebtoken + jjwt-impl + ${jsonwebtoken.version} + + + + io.jsonwebtoken + jjwt-jackson + ${jsonwebtoken.version} + + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + mongodb + test + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${openapi.version} + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc-openapi.version} + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.micrometer + micrometer-registry-prometheus + runtime + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 21 + 21 + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + --enable-preview + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-version} + + + + prepare-agent + + + + report + prepare-package + + report + + + + post-test-report + verify + + report + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin-version} + + + **/*Test.java + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin-version} + + + + integration-test + verify + + + + + + + + + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/FlightSearchApiApplication.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/FlightSearchApiApplication.java new file mode 100644 index 000000000..1620d957e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/FlightSearchApiApplication.java @@ -0,0 +1,27 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * The main entry point for the Flight Search Api application with Couchbase integration. + * This class is responsible for launching the Spring Boot application, which initializes all necessary components, + * including configurations for Couchbase, and starts the application context. + * The {@link SpringApplication#run(Class, String...)} method is invoked to start the application. + */ +@SpringBootApplication +public class FlightSearchApiApplication { + + /** + * The main method which serves as the entry point to launch the Spring Boot application. + * This method initializes and runs the {@link SpringApplication} to start the application. + * It automatically configures the application context and any required components. + * + * @param args Command-line arguments passed to the application. + * These are forwarded to the {@link SpringApplication#run(Class, String...)} method. + */ + public static void main(String[] args) { + SpringApplication.run(FlightSearchApiApplication.class, args); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/config/SecurityConfig.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/config/SecurityConfig.java new file mode 100644 index 000000000..74cc572c0 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/config/SecurityConfig.java @@ -0,0 +1,115 @@ +package com.example.demo.auth.config; + +import com.example.demo.auth.filter.CustomBearerTokenAuthenticationFilter; +import com.example.demo.auth.security.CustomAuthenticationEntryPoint; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +/** + * Configuration class for security settings of the application. + * This class sets up the authentication and authorization mechanisms using Spring Security. + * It defines the session management strategy, security filter chain, CORS configurations, + * and password encoding strategy. + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@EnableMethodSecurity +public class SecurityConfig { + + /** + * Configures the session authentication strategy for the application. + * Registers a session authentication strategy that tracks authenticated sessions + * using a {@link SessionRegistryImpl}. + * + * @return the session authentication strategy to be used. + */ + @Bean + protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { + return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); + } + + /** + * Configures the security filter chain for handling HTTP security. + * + * @param httpSecurity the {@link HttpSecurity} to configure. + * @param customBearerTokenAuthenticationFilter a custom filter for handling bearer token authentication. + * @param customAuthenticationEntryPoint a custom entry point for authentication failures. + * @return the configured {@link SecurityFilterChain}. + * @throws Exception if an error occurs during configuration. + */ + @Bean + public SecurityFilterChain filterChain( + final HttpSecurity httpSecurity, + final CustomBearerTokenAuthenticationFilter customBearerTokenAuthenticationFilter, + final CustomAuthenticationEntryPoint customAuthenticationEntryPoint + ) throws Exception { + + httpSecurity + .exceptionHandling(customizer -> customizer.authenticationEntryPoint(customAuthenticationEntryPoint)) + .cors(customizer -> customizer.configurationSource(corsConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(customizer -> customizer + .requestMatchers(HttpMethod.POST, "/api/v1/authentication/**").permitAll() + .requestMatchers( + "/swagger-ui/**", + "/swagger-ui.html", + "/v2/api-docs/**", + "/v3/api-docs/**", + "/actuator/**" + ).permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(customizer -> customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(customBearerTokenAuthenticationFilter, BearerTokenAuthenticationFilter.class); + + return httpSecurity.build(); + } + + /** + * Provides the CORS configuration for the application. + * Allows all origins, methods, and headers for cross-origin requests. + * + * @return the {@link CorsConfigurationSource} with the specified configurations. + */ + private CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("*")); + configuration.setAllowedMethods(List.of("*")); + configuration.setAllowedHeaders(List.of("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + /** + * Provides the password encoder for the application. + * Configures a {@link BCryptPasswordEncoder} for securely hashing passwords. + * + * @return the {@link PasswordEncoder} to be used. + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/config/TokenConfigurationParameter.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/config/TokenConfigurationParameter.java new file mode 100644 index 000000000..a2c7a7c3c --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/config/TokenConfigurationParameter.java @@ -0,0 +1,63 @@ +package com.example.demo.auth.config; + +import com.example.demo.auth.model.enums.ConfigurationParameter; +import com.example.demo.auth.utils.KeyConverter; +import lombok.Getter; +import org.springframework.context.annotation.Configuration; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Configuration class for token-related parameters. + * This class retrieves and initializes token configuration settings, including + * issuer, access token expiration, refresh token expiration, public key, and private key. + * The parameters are loaded from predefined configuration constants. + */ +@Getter +@Configuration +public class TokenConfigurationParameter { + + private final String issuer; + private final int accessTokenExpireMinute; + private final int refreshTokenExpireDay; + private final PublicKey publicKey; + private final PrivateKey privateKey; + + /** + * Initializes the token configuration parameters. + *

+ * The parameters are retrieved from the {@link ConfigurationParameter} class: + *

    + *
  • Issuer: {@code ConfigurationParameter.ISSUER}
  • + *
  • Access token expiration (minutes): {@code ConfigurationParameter.AUTH_ACCESS_TOKEN_EXPIRE_MINUTE}
  • + *
  • Refresh token expiration (days): {@code ConfigurationParameter.AUTH_REFRESH_TOKEN_EXPIRE_DAY}
  • + *
  • Public key: {@code ConfigurationParameter.AUTH_PUBLIC_KEY}
  • + *
  • Private key: {@code ConfigurationParameter.AUTH_PRIVATE_KEY}
  • + *
+ * Key values are converted using the {@link KeyConverter} utility class. + *

+ */ + public TokenConfigurationParameter() { + + this.issuer = ConfigurationParameter.ISSUER.getDefaultValue(); + + this.accessTokenExpireMinute = Integer.parseInt( + ConfigurationParameter.AUTH_ACCESS_TOKEN_EXPIRE_MINUTE.getDefaultValue() + ); + + this.refreshTokenExpireDay = Integer.parseInt( + ConfigurationParameter.AUTH_REFRESH_TOKEN_EXPIRE_DAY.getDefaultValue() + ); + + this.publicKey = KeyConverter.convertPublicKey( + ConfigurationParameter.AUTH_PUBLIC_KEY.getDefaultValue() + ); + + this.privateKey = KeyConverter.convertPrivateKey( + ConfigurationParameter.AUTH_PRIVATE_KEY.getDefaultValue() + ); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/controller/AuthController.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/controller/AuthController.java new file mode 100644 index 000000000..e0f4bfe35 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/controller/AuthController.java @@ -0,0 +1,133 @@ +package com.example.demo.auth.controller; + +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.request.LoginRequest; +import com.example.demo.auth.model.dto.request.RegisterRequest; +import com.example.demo.auth.model.dto.request.TokenInvalidateRequest; +import com.example.demo.auth.model.dto.request.TokenRefreshRequest; +import com.example.demo.auth.model.dto.response.TokenResponse; +import com.example.demo.auth.model.mapper.TokenToTokenResponseMapper; +import com.example.demo.auth.service.LoginService; +import com.example.demo.auth.service.LogoutService; +import com.example.demo.auth.service.RefreshTokenService; +import com.example.demo.auth.service.RegisterService; +import com.example.demo.common.model.dto.response.CustomResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller that handles user authentication and authorization operations. + * This controller provides endpoints for user registration, login, token refreshing, and logout functionality. + * It delegates the business logic to the appropriate services: {@link RegisterService}, {@link LoginService}, + * {@link RefreshTokenService}, and {@link LogoutService}. + */ +@RestController +@RequestMapping("/api/v1/authentication/user") +@RequiredArgsConstructor +@Validated +@Tag(name = "Authentication", description = "Handles user authentication and authorization operations.") +public class AuthController { + + private final RegisterService registerService; + + private final LoginService loginService; + + private final RefreshTokenService refreshTokenService; + + private final LogoutService logoutService; + + private final TokenToTokenResponseMapper tokenToTokenResponseMapper = TokenToTokenResponseMapper.initialize(); + + /** + * Endpoint to register a new user. + * + * @param registerRequest The {@link RegisterRequest} object containing user registration details. + * @return A {@link CustomResponse} indicating the success of the registration operation. + */ + @Operation( + summary = "Register a new user", + description = "Registers a new user in the system.", + responses = { + @ApiResponse(responseCode = "200", description = "User successfully registered"), + @ApiResponse(responseCode = "400", description = "Invalid request data"), + @ApiResponse(responseCode = "409", description = "User already exists") + } + ) + @PostMapping("/register") + public CustomResponse registerAdmin(@RequestBody @Valid final RegisterRequest registerRequest) { + registerService.registerUser(registerRequest); + return CustomResponse.SUCCESS; + } + + /** + * Endpoint for user login. + * + * @param loginRequest The {@link LoginRequest} object containing user login credentials. + * @return A {@link CustomResponse} containing a {@link TokenResponse} with the generated tokens. + */ + @Operation( + summary = "User login", + description = "Authenticates a user and returns an access and refresh token.", + responses = { + @ApiResponse(responseCode = "200", description = "Login successful"), + @ApiResponse(responseCode = "401", description = "Invalid login credentials") + } + ) + @PostMapping("/login") + public CustomResponse loginAdmin(@RequestBody @Valid final LoginRequest loginRequest) { + final Token token = loginService.login(loginRequest); + final TokenResponse tokenResponse = tokenToTokenResponseMapper.map(token); + return CustomResponse.successOf(tokenResponse); + } + + /** + * Endpoint to refresh an expired access token. + * + * @param tokenRefreshRequest The {@link TokenRefreshRequest} object containing the refresh token. + * @return A {@link CustomResponse} containing a {@link TokenResponse} with the refreshed tokens. + */ + @Operation( + summary = "Refresh access token", + description = "Refreshes an expired access token using a valid refresh token.", + responses = { + @ApiResponse(responseCode = "200", description = "Token successfully refreshed"), + @ApiResponse(responseCode = "400", description = "Invalid refresh token"), + @ApiResponse(responseCode = "401", description = "Refresh token expired or invalid") + } + ) + @PostMapping("/refresh-token") + public CustomResponse refreshToken(@RequestBody @Valid final TokenRefreshRequest tokenRefreshRequest) { + final Token token = refreshTokenService.refreshToken(tokenRefreshRequest); + final TokenResponse tokenResponse = tokenToTokenResponseMapper.map(token); + return CustomResponse.successOf(tokenResponse); + } + + /** + * Endpoint to log out a user. + * + * @param tokenInvalidateRequest The {@link TokenInvalidateRequest} object containing the token to be invalidated. + * @return A {@link CustomResponse} indicating the success of the logout operation. + */ + @Operation( + summary = "Log out a user", + description = "Invalidates the provided token, effectively logging out the user.", + responses = { + @ApiResponse(responseCode = "200", description = "Logout successful"), + @ApiResponse(responseCode = "400", description = "Invalid token provided") + } + ) + @PostMapping("/logout") + public CustomResponse logout(@RequestBody @Valid final TokenInvalidateRequest tokenInvalidateRequest) { + logoutService.logout(tokenInvalidateRequest); + return CustomResponse.SUCCESS; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/PasswordNotValidException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/PasswordNotValidException.java new file mode 100644 index 000000000..b667ae404 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/PasswordNotValidException.java @@ -0,0 +1,40 @@ +package com.example.demo.auth.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when a password is not valid. + * This exception extends {@link RuntimeException} and provides a default message + * for invalid passwords. It also allows for custom messages to be appended to the default message. + */ +public class PasswordNotValidException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -8180169011759975985L; + + public static final HttpStatus STATUS = HttpStatus.BAD_REQUEST; + + private static final String DEFAULT_MESSAGE = """ + Password is not valid! + """; + + /** + * Constructs a new {@code PasswordNotValidException} with the default message. + */ + public PasswordNotValidException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new {@code PasswordNotValidException} with the default message + * and appends a custom message. + * + * @param message the custom message to append to the default message. + */ + public PasswordNotValidException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/RoleNotFoundException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/RoleNotFoundException.java new file mode 100644 index 000000000..a93b3de5f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/RoleNotFoundException.java @@ -0,0 +1,41 @@ +package com.example.demo.auth.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when a requested role is not found. + * This exception extends {@link RuntimeException} and is typically used in scenarios + * where a role lookup operation fails. It provides a default message and allows + * for an additional custom message to be appended. + */ +public class RoleNotFoundException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -4353606558549954783L; + + public static final HttpStatus STATUS = HttpStatus.NOT_FOUND; + + private static final String DEFAULT_MESSAGE = """ + Role not found! + """; + + /** + * Constructs a new {@code RoleNotFoundException} with the default message. + */ + public RoleNotFoundException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new {@code RoleNotFoundException} with the default message + * and appends a custom message. + * + * @param message the custom message to append to the default message. + */ + public RoleNotFoundException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/TokenAlreadyInvalidatedException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/TokenAlreadyInvalidatedException.java new file mode 100644 index 000000000..be424d013 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/TokenAlreadyInvalidatedException.java @@ -0,0 +1,40 @@ +package com.example.demo.auth.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when an attempt is made to invalidate a token that is already invalidated. + * This exception extends {@link RuntimeException} and is typically used in token management + * scenarios to indicate that a token cannot be invalidated again. + */ +public class TokenAlreadyInvalidatedException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 66846615891590622L; + + public static final HttpStatus STATUS = HttpStatus.BAD_REQUEST; + + private static final String DEFAULT_MESSAGE = """ + Token is already invalidated! + """; + + /** + * Constructs a new {@code TokenAlreadyInvalidatedException} with the default message. + */ + public TokenAlreadyInvalidatedException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new {@code TokenAlreadyInvalidatedException} with the default message + * and includes the specified token ID in the message. + * + * @param tokenId the ID of the token that was already invalidated. + */ + public TokenAlreadyInvalidatedException(final String tokenId) { + super(DEFAULT_MESSAGE + " TokenID = " + tokenId); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UnAuthorizeAttemptException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UnAuthorizeAttemptException.java new file mode 100644 index 000000000..82eff6ccb --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UnAuthorizeAttemptException.java @@ -0,0 +1,31 @@ +package com.example.demo.auth.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when an unauthorized attempt is made to perform an action, + * such as creating a to-do item without proper permissions. + * This exception extends {@link RuntimeException} and is typically used in scenarios + * where access control or authorization checks fail. + */ +public class UnAuthorizeAttemptException extends RuntimeException{ + + @Serial + private static final long serialVersionUID = 668466158918970622L; + + public static final HttpStatus STATUS = HttpStatus.UNAUTHORIZED; + + private static final String DEFAULT_MESSAGE = """ + You do not have permission to create a to-do item. + """; + + /** + * Constructs a new {@code UnAuthorizeAttemptException} with the default message. + */ + public UnAuthorizeAttemptException() { + super(DEFAULT_MESSAGE); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserAlreadyExistException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserAlreadyExistException.java new file mode 100644 index 000000000..c82734ab5 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserAlreadyExistException.java @@ -0,0 +1,40 @@ +package com.example.demo.auth.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when an attempt is made to create a user that already exists. + * This exception extends {@link RuntimeException} and is typically used to indicate + * a conflict during user registration or creation. + */ +public class UserAlreadyExistException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -6595921257346957788L; + + public static final HttpStatus STATUS = HttpStatus.CONFLICT; + + private static final String DEFAULT_MESSAGE = """ + User already exist! + """; + + /** + * Constructs a new {@code UserAlreadyExistException} with the default message. + */ + public UserAlreadyExistException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new {@code UserAlreadyExistException} with the default message + * and appends a custom message. + * + * @param message the custom message to append to the default message. + */ + public UserAlreadyExistException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserNotFoundException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserNotFoundException.java new file mode 100644 index 000000000..7683133c6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserNotFoundException.java @@ -0,0 +1,40 @@ +package com.example.demo.auth.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when a requested user is not found in the system. + * This exception extends {@link RuntimeException} and is typically used to indicate + * that a user-related query has failed due to the absence of the user. + */ +public class UserNotFoundException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 6032856035243860910L; + + public static final HttpStatus STATUS = HttpStatus.NOT_FOUND; + + private static final String DEFAULT_MESSAGE = """ + User not found! + """; + + /** + * Constructs a new {@code UserNotFoundException} with the default message. + */ + public UserNotFoundException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new {@code UserNotFoundException} with the default message + * and appends a custom message. + * + * @param message the custom message to append to the default message. + */ + public UserNotFoundException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserStatusNotValidException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserStatusNotValidException.java new file mode 100644 index 000000000..924bd42c2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/exception/UserStatusNotValidException.java @@ -0,0 +1,40 @@ +package com.example.demo.auth.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when a user's status is deemed invalid for a specific operation. + * This exception extends {@link RuntimeException} and is typically used in scenarios + * where user-related validations fail due to an invalid status. + */ +public class UserStatusNotValidException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -4774378445348925475L; + + public static final HttpStatus STATUS = HttpStatus.BAD_REQUEST; + + private static final String DEFAULT_MESSAGE = """ + User status is not valid! + """; + + /** + * Constructs a new {@code UserStatusNotValidException} with the default message. + */ + public UserStatusNotValidException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new {@code UserStatusNotValidException} with the default message + * and appends a custom message. + * + * @param message the custom message to append to the default message. + */ + public UserStatusNotValidException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/filter/CustomBearerTokenAuthenticationFilter.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/filter/CustomBearerTokenAuthenticationFilter.java new file mode 100644 index 000000000..84bdfb2c6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/filter/CustomBearerTokenAuthenticationFilter.java @@ -0,0 +1,74 @@ +package com.example.demo.auth.filter; + +import com.example.demo.auth.model.Token; +import com.example.demo.auth.service.InvalidTokenService; +import com.example.demo.auth.service.TokenService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * Custom filter for handling Bearer token authentication. + * This filter extracts the Bearer token from the HTTP `Authorization` header, validates the token, + * checks if it has been invalidated, and sets the authentication in the {@link SecurityContextHolder}. + * Extends {@link OncePerRequestFilter} to ensure the filter is executed only once per request. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CustomBearerTokenAuthenticationFilter extends OncePerRequestFilter { + + private final TokenService tokenService; + private final InvalidTokenService invalidTokenService; + + /** + * Performs filtering logic for each HTTP request to validate Bearer tokens. + * + * @param httpServletRequest the current HTTP request. + * @param httpServletResponse the current HTTP response. + * @param filterChain the filter chain to delegate further request processing. + * @throws ServletException if an error occurs during request processing. + * @throws IOException if an input or output error occurs during request processing. + */ + @Override + protected void doFilterInternal(@NonNull final HttpServletRequest httpServletRequest, + @NonNull final HttpServletResponse httpServletResponse, + @NonNull final FilterChain filterChain) throws ServletException, IOException { + + log.debug("API Request was secured with Security!"); + + final String authorizationHeader = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); + + if (Token.isBearerToken(authorizationHeader)) { + + final String jwt = Token.getJwt(authorizationHeader); + + tokenService.verifyAndValidate(jwt); + + final String tokenId = tokenService.getId(jwt); + + invalidTokenService.checkForInvalidityOfToken(tokenId); + + final UsernamePasswordAuthenticationToken authentication = tokenService + .getAuthentication(jwt); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + } + + filterChain.doFilter(httpServletRequest,httpServletResponse); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/Token.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/Token.java new file mode 100644 index 000000000..c41b43221 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/Token.java @@ -0,0 +1,44 @@ +package com.example.demo.auth.model; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.util.StringUtils; + +/** + * Represents an access and refresh token used for authentication and authorization. + * This class contains information about the access token, its expiration time, and the refresh token. + * It also provides utility methods for handling tokens in the form of "Bearer" tokens, which are commonly used + * in HTTP Authorization headers. + */ +@Getter +@Builder +public class Token { + + private String accessToken; + private Long accessTokenExpiresAt; + private String refreshToken; + + private static final String TOKEN_PREFIX = "Bearer "; + + /** + * Checks if the given authorization header contains a valid bearer token. + * + * @param authorizationHeader The authorization header to be checked. + * @return {@code true} if the authorization header contains a valid bearer token, otherwise {@code false}. + */ + public static boolean isBearerToken(final String authorizationHeader) { + return StringUtils.hasText(authorizationHeader) && + authorizationHeader.startsWith(TOKEN_PREFIX); + } + + /** + * Extracts the JWT token from the authorization header. + * + * @param authorizationHeader The authorization header containing the token. + * @return The JWT token extracted from the authorization header. + */ + public static String getJwt(final String authorizationHeader) { + return authorizationHeader.replace(TOKEN_PREFIX, ""); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/User.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/User.java new file mode 100644 index 000000000..498710765 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/User.java @@ -0,0 +1,32 @@ +package com.example.demo.auth.model; + +import com.example.demo.auth.model.enums.UserStatus; +import com.example.demo.auth.model.enums.UserType; +import com.example.demo.common.model.BaseDomainModel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +/** + * Represents an access and refresh token used for authentication and authorization. + * This class contains information about the access token, its expiration time, and the refresh token. + * It also provides utility methods for handling tokens in the form of "Bearer" tokens, which are commonly used + * in HTTP Authorization headers. + */ +@Getter +@Setter +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class User extends BaseDomainModel { + + private String id; + private String email; + private String firstName; + private String lastName; + private String phoneNumber; + private UserType userType; + private UserStatus userStatus; + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/LoginRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/LoginRequest.java new file mode 100644 index 000000000..3c9ac0153 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/LoginRequest.java @@ -0,0 +1,24 @@ +package com.example.demo.auth.model.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents a request for user login containing email and password. + * This class is used to capture login credentials when a user attempts to log in. + * It is validated to ensure that both the email and password fields are not blank. + */ +@Getter +@Setter +@Builder +public class LoginRequest { + + @NotBlank + private String email; + + @NotBlank + private String password; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/RegisterRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/RegisterRequest.java new file mode 100644 index 000000000..bc89eed95 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/RegisterRequest.java @@ -0,0 +1,42 @@ +package com.example.demo.auth.model.dto.request; + +import com.example.demo.auth.model.enums.UserType; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.*; + +/** + * Represents a request for user registration containing the necessary details + * to create a new user in the system. + * This class is used to capture the data required for registering a user, including + * their email, password, first and last name, phone number, and user type. It also + * enforces validation rules on the fields to ensure data integrity. + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RegisterRequest { + + @Email(message = "Please enter a valid e-mail address") + @Size(min = 7, message = "Minimum e-mail length is 7 characters.") + private String email; + + @Size(min = 8, message = "Minimum password length is 8 characters.") + private String password; + + @NotBlank(message = "First name can't be blank.") + private String firstName; + + @NotBlank(message = "Last name can't be blank.") + private String lastName; + + @NotBlank(message = "Phone number can't be blank.") + @Size(min = 11, max = 20, message = "Phone number must be between 11 and 20 characters.") + private String phoneNumber; + + private UserType userType; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/TokenInvalidateRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/TokenInvalidateRequest.java new file mode 100644 index 000000000..f611e48cd --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/TokenInvalidateRequest.java @@ -0,0 +1,25 @@ +package com.example.demo.auth.model.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +/** + * Represents a request to invalidate a token, typically used to log out a user + * by invalidating their access and refresh tokens. + * This class is used to capture the access token and refresh token that need to be invalidated. + * Both tokens are required for the invalidation process to be carried out. + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TokenInvalidateRequest { + + @NotBlank + private String accessToken; + + @NotBlank + private String refreshToken; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/TokenRefreshRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/TokenRefreshRequest.java new file mode 100644 index 000000000..96174a688 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/request/TokenRefreshRequest.java @@ -0,0 +1,21 @@ +package com.example.demo.auth.model.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +/** + * Represents a request to refresh a token, typically used when a user needs a new access token + * using a valid refresh token. + * This class captures the refresh token, which is required for generating a new access token. + */ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TokenRefreshRequest { + + @NotBlank + private String refreshToken; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/response/TokenResponse.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/response/TokenResponse.java new file mode 100644 index 000000000..f24113f13 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/dto/response/TokenResponse.java @@ -0,0 +1,21 @@ +package com.example.demo.auth.model.dto.response; + +import lombok.*; + +/** + * Represents a request to refresh a token, typically used when a user needs a new access token + * using a valid refresh token. + * This class captures the refresh token, which is required for generating a new access token. + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TokenResponse { + + private String accessToken; + private Long accessTokenExpiresAt; + private String refreshToken; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/entity/InvalidTokenEntity.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/entity/InvalidTokenEntity.java new file mode 100644 index 000000000..2d77f4afe --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/entity/InvalidTokenEntity.java @@ -0,0 +1,32 @@ +package com.example.demo.auth.model.entity; + +import com.example.demo.common.model.entity.BaseEntity; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +/** + * Represents an entity for storing invalidated tokens in MongoDB. + * Each token is uniquely identified by {@code tokenId}. + * Extends {@link BaseEntity} for common timestamp fields. + */ +@Getter +@Setter +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "invalid-token-collection") +public class InvalidTokenEntity extends BaseEntity { + + @Id + @Indexed(unique = true) + private String id; + + @Field(name = "TOKEN_ID") + private String tokenId; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/entity/UserEntity.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/entity/UserEntity.java new file mode 100644 index 000000000..082475de1 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/entity/UserEntity.java @@ -0,0 +1,80 @@ +package com.example.demo.auth.model.entity; + +import com.example.demo.auth.model.enums.TokenClaims; +import com.example.demo.auth.model.enums.UserStatus; +import com.example.demo.auth.model.enums.UserType; +import com.example.demo.common.model.entity.BaseEntity; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a user entity in the system, storing personal and authentication details. + * Mapped to the MongoDB {@code user-collection}, with {@code id} as the unique identifier. + * Extends {@link BaseEntity}, inheriting common properties like timestamps. + */ +@Getter +@Setter +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "user-collection") +public class UserEntity extends BaseEntity { + + @Id + @Indexed(unique = true) + private String id; + + @Field(name = "EMAIL") + @Indexed(unique = true) + private String email; + + @Field(name = "PASSWORD") + private String password; + + @Field(name = "FIRST_NAME") + private String firstName; + + @Field(name = "LAST_NAME") + private String lastName; + + @Field(name = "PHONE_NUMBER") + private String phoneNumber; + + @Field(name = "USER_TYPE") + private UserType userType; + + @Builder.Default + @Field(name = "USER_STATUS") + private UserStatus userStatus = UserStatus.ACTIVE; + + /** + * Generates a map of claims associated with the user. + * This method creates a map of key-value pairs representing the user's claims, + * which can be used for JWT token generation or other purposes where user-related + * data needs to be included. + * + * @return a map containing the user's claims. + */ + public Map getClaims() { + final Map claims = new HashMap<>(); + + claims.put(TokenClaims.USER_ID.getValue(), this.id); + claims.put(TokenClaims.USER_TYPE.getValue(), this.userType); + claims.put(TokenClaims.USER_STATUS.getValue(), this.userStatus); + claims.put(TokenClaims.USER_FIRST_NAME.getValue(), this.firstName); + claims.put(TokenClaims.USER_LAST_NAME.getValue(), this.lastName); + claims.put(TokenClaims.USER_EMAIL.getValue(), this.email); + claims.put(TokenClaims.USER_PHONE_NUMBER.getValue(), this.phoneNumber); + + return claims; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/ConfigurationParameter.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/ConfigurationParameter.java new file mode 100644 index 000000000..fe64138fe --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/ConfigurationParameter.java @@ -0,0 +1,63 @@ +package com.example.demo.auth.model.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Enum representing the configuration parameters used in the application. + * Each parameter corresponds to a specific configuration value, such as issuer, token expiration times, + * and keys used for authentication and encryption. + */ +@Getter +@RequiredArgsConstructor +public enum ConfigurationParameter { + + ISSUER("ISSUER"), + + AUTH_ACCESS_TOKEN_EXPIRE_MINUTE("30"), + AUTH_REFRESH_TOKEN_EXPIRE_DAY("1"), + AUTH_PUBLIC_KEY(""" + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1HmZ3A379M6Rv9UnMt9R + Wq0a6bpcnoOWJxTi2exwnecW3r1X1PjeUvsDogy7RYjhlxU0G+1r38gPWfUW2FNd + tsa3H+FDhJ6dcNc4uKVYPsiVJukHi4NrvWA8E8dPdLW1lNcijr4PqXjvZTLoS1QX + f30wnNLBNDwdPXTESodi/n87VoSH2ChgLZUVfoS3m/NlUN8Z58gGxRcpUyjl+MmC + hD2cfyWr2xdxKd+UQMrd36LfyoWh0IlONlxo0H5x8JIwlziLbPEAh7dJ9QYM0b5G + msXBAzrILvW5+POSq4u1vNlzSwdLe+AZ6bnCwQVrqvMn/I7JT+4+lY8BsP/gMRQL + awIDAQAB + -----END PUBLIC KEY----- + """), + AUTH_PRIVATE_KEY(""" + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUeZncDfv0zpG/ + 1Scy31FarRrpulyeg5YnFOLZ7HCd5xbevVfU+N5S+wOiDLtFiOGXFTQb7WvfyA9Z + 9RbYU122xrcf4UOEnp1w1zi4pVg+yJUm6QeLg2u9YDwTx090tbWU1yKOvg+peO9l + MuhLVBd/fTCc0sE0PB09dMRKh2L+fztWhIfYKGAtlRV+hLeb82VQ3xnnyAbFFylT + KOX4yYKEPZx/JavbF3Ep35RAyt3fot/KhaHQiU42XGjQfnHwkjCXOIts8QCHt0n1 + BgzRvkaaxcEDOsgu9bn485Kri7W82XNLB0t74BnpucLBBWuq8yf8jslP7j6VjwGw + /+AxFAtrAgMBAAECggEARWpw5N7AuQsfvN+DjfA9oPU6/K9BARyWWrBNKMtBQ6Uy + 6JRNdKvV3qBZYIDuUdpVcUmhG5qmipbOxSH4U7ZwwH0NaOHscBBt+WanBlQmj2Ry + riKlr2PBOD6Pghq0j7mp2DWs+ZuIfGKhO5u1Hp8bijA5SJLmQg19tA1I79xpcCFC + flY8WaBKJCTKtH3sisS9IqAtGx3+O2fwzk5oSA6DDuX+45zTLS1vlucwhTUTDE4j + UitYM5CnW9LzJ72ofWZBE29twECXQa+7YYXYniqUOzYZVhy7OiwYT9HH2fDGurmL + F4adtcmVS6CUM6OYSV8z1DXbwLcidTqwiAj9uZ8suQKBgQD2hb/Bimgoh0dT6q9N + pORikLW9L/6q3TuAfNJrrP63dwgooeCcHdk8Wh1zFkNibmfdhHQM5Hu3pQq0oxtG + XiH1WYhluDAjyq6ahvYasgGtdzh279VcicleP9x6B6tmWop+a9kM+Yh2r3Yb5KXr + ZyFWLeoEkCyeXdVr1tds119Q/QKBgQDcpMKXIAiayZmcHU03pNrDGhs7m2B18S6Z + WcXSQRf1d98wbzzfsrbHM9k8gX5VGvZb+rwl95SyevT1LSaGM44On4WXjCUlpUnb + 8+mKONra8yXuWutB1aclPQddj4HAFGikJyBP45e/SXNTBPrK5cm/HULbgd76FxHv + QQZ2EbSOhwKBgA5pHR98LsCHv+So6Fx6khss6GLJxnJIgmztXwOKVk11ONXfOJkH + qaY8glIy7/d2Cr5JOttyE8VVcX3Dtxly8Ts9Y5rGnJHLDE/eKc6/rxdry7IwLOG+ + 8DWBOCst/Zf7HPNs7IA0qgR+F0JkKErNeYZnIrHnl6QeShaGtYsYP+slAoGBANRU + yd5dOWqb73NIz3Jo9w0iJmrqT52wh8OTnMeFVOUogmQ96Drt5O82eiu8AjMsS0Cg + vkdbRoGryefXl2c2XdK8uPbqKyVbNwSwaWJW7GYf77S9UgB89ujjHh9vZtHN0hWG + gZXf07yFlrGh7Scsk0WThy9uf4H0iZHQ5cLhrvwpAoGAAj3uW/aAJDhfHHVgBGhC + la8OzEqY8UbqMYrXKfrmYOzvFYKWd5BgrZvgINhSmjxMeil952gHVsH/td+KUyqe + dxTAgV9WVn8KVz6KoEKJV5CarN0IOl5fZVJz1i0Q5t6VdUXLC6teh4ByrEAlsRgi + h2UWcQCA6KqrmJbR2q6yj+s= + -----END PRIVATE KEY----- + """); + + private final String defaultValue; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/TokenClaims.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/TokenClaims.java new file mode 100644 index 000000000..25d34641e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/TokenClaims.java @@ -0,0 +1,31 @@ +package com.example.demo.auth.model.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Enum representing various claims used in a JWT token. + * Each enum constant represents a claim, along with its associated key (string value) used in the token. + * These claims are typically used to store information about the user, token metadata, or other context-specific data. + */ +@Getter +@RequiredArgsConstructor +public enum TokenClaims { + + JWT_ID("jti"), + USER_ID("userId"), + USER_TYPE("userType"), + USER_STATUS("userStatus"), + USER_FIRST_NAME("userFirstName"), + USER_LAST_NAME("userLastName"), + USER_EMAIL("userEmail"), + USER_PHONE_NUMBER("userPhoneNumber"), + STORE_TITLE("storeTitle"), + ISSUED_AT("iat"), + EXPIRES_AT("exp"), + ALGORITHM("alg"), + TYP("typ"); + + private final String value; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/TokenType.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/TokenType.java new file mode 100644 index 000000000..a3c7f8299 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/TokenType.java @@ -0,0 +1,18 @@ +package com.example.demo.auth.model.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Enum representing the different types of tokens that can be used in authentication schemes. + * Currently, only the "Bearer" token type is supported, which is commonly used in OAuth2 and JWT authentication. + */ +@Getter +@RequiredArgsConstructor +public enum TokenType { + + BEARER("Bearer"); + + private final String value; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/UserStatus.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/UserStatus.java new file mode 100644 index 000000000..9bca7043c --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/UserStatus.java @@ -0,0 +1,11 @@ +package com.example.demo.auth.model.enums; + +/** + * Enum representing the different statuses a user can have in the system. + * This can be used to track and manage user account states in the application. + */ +public enum UserStatus { + ACTIVE, + PASSIVE, + SUSPENDED +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/UserType.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/UserType.java new file mode 100644 index 000000000..3cd5d80a2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/enums/UserType.java @@ -0,0 +1,10 @@ +package com.example.demo.auth.model.enums; + +/** + * Enum representing the different types of users in the system. + * This is used to differentiate between regular users and administrative users. + */ +public enum UserType { + USER, + ADMIN +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/RegisterRequestToUserEntityMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/RegisterRequestToUserEntityMapper.java new file mode 100644 index 000000000..8ad742edb --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/RegisterRequestToUserEntityMapper.java @@ -0,0 +1,45 @@ +package com.example.demo.auth.model.mapper; + +import com.example.demo.auth.model.dto.request.RegisterRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.common.model.mapper.BaseMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting {@link RegisterRequest} to {@link UserEntity}. + * This interface extends {@link BaseMapper} and provides the mapping logic from the + * {@link RegisterRequest} DTO to a {@link UserEntity} entity. It includes a custom mapping method + * for saving the {@link RegisterRequest} to the {@link UserEntity}. + */ +@Mapper +public interface RegisterRequestToUserEntityMapper extends BaseMapper { + + /** + * Maps a {@link RegisterRequest} to a {@link UserEntity} for saving purposes. + * + * @param registerRequest The {@link RegisterRequest} object to be mapped. + * @return A {@link UserEntity} populated with the data from the given {@link RegisterRequest}. + */ + @Named("mapForSaving") + default UserEntity mapForSaving(RegisterRequest registerRequest) { + return UserEntity.builder() + .email(registerRequest.getEmail()) + .firstName(registerRequest.getFirstName()) + .lastName(registerRequest.getLastName()) + .phoneNumber(registerRequest.getPhoneNumber()) + .userType(registerRequest.getUserType()) + .build(); + } + + /** + * Initializes and returns an instance of {@link RegisterRequestToUserEntityMapper}. + * + * @return An initialized {@link RegisterRequestToUserEntityMapper} instance. + */ + static RegisterRequestToUserEntityMapper initialize() { + return Mappers.getMapper(RegisterRequestToUserEntityMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/TokenToTokenResponseMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/TokenToTokenResponseMapper.java new file mode 100644 index 000000000..3c182ae26 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/TokenToTokenResponseMapper.java @@ -0,0 +1,38 @@ +package com.example.demo.auth.model.mapper; + +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.response.TokenResponse; +import com.example.demo.common.model.mapper.BaseMapper; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting {@link Token} to {@link TokenResponse}. + * This interface extends {@link BaseMapper} and provides the mapping logic from the + * {@link Token} entity to a {@link TokenResponse} DTO. It includes a method for mapping a + * {@link Token} to a {@link TokenResponse}. + */ +@Mapper +public interface TokenToTokenResponseMapper extends BaseMapper { + + /** + * Maps a {@link Token} to a {@link TokenResponse}. + * + * @param source The {@link Token} object to be mapped. + * @return A {@link TokenResponse} object containing the mapped data from the {@link Token}. + */ + @Override + TokenResponse map(Token source); + + /** + * Initializes and returns an instance of {@link TokenToTokenResponseMapper}. + * This method provides a way to retrieve the singleton mapper instance for + * converting {@link Token} to {@link TokenResponse}. + * + * @return An initialized {@link TokenToTokenResponseMapper} instance. + */ + static TokenToTokenResponseMapper initialize() { + return Mappers.getMapper(TokenToTokenResponseMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/UserEntityToUserMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/UserEntityToUserMapper.java new file mode 100644 index 000000000..3420a01d1 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/model/mapper/UserEntityToUserMapper.java @@ -0,0 +1,36 @@ +package com.example.demo.auth.model.mapper; + +import com.example.demo.auth.model.User; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.common.model.mapper.BaseMapper; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting {@link UserEntity} to {@link User}. + * This interface extends {@link BaseMapper} and provides the mapping logic from the + * {@link UserEntity} entity to a {@link User} DTO. It includes a method for mapping a + * {@link UserEntity} to a {@link User}. + */ +@Mapper +public interface UserEntityToUserMapper extends BaseMapper { + + /** + * Maps a {@link UserEntity} to a {@link User}. + * + * @param source The {@link UserEntity} object to be mapped. + * @return A {@link User} object containing the mapped data from the {@link UserEntity}. + */ + @Override + User map(UserEntity source); + + /** + * Initializes and returns an instance of {@link UserEntityToUserMapper}. + * + * @return An initialized {@link UserEntityToUserMapper} instance. + */ + static UserEntityToUserMapper initialize() { + return Mappers.getMapper(UserEntityToUserMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/repository/InvalidTokenRepository.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/repository/InvalidTokenRepository.java new file mode 100644 index 000000000..c8a0609a4 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/repository/InvalidTokenRepository.java @@ -0,0 +1,24 @@ +package com.example.demo.auth.repository; + +import com.example.demo.auth.model.entity.InvalidTokenEntity; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.Optional; + +/** + * Repository interface for managing {@link InvalidTokenEntity} objects in Couchbase. + * This interface extends {@link MongoRepository} to provide CRUD operations for the {@link InvalidTokenEntity}. + * It also includes custom query methods to interact with the data store. + */ +public interface InvalidTokenRepository extends MongoRepository { + + /** + * Finds an {@link InvalidTokenEntity} by its token ID. + * This method is used to retrieve an invalid token entity based on the unique token ID. + * + * @param tokenId The unique ID of the token to search for. + * @return An {@link Optional} containing the {@link InvalidTokenEntity} if found, or {@link Optional#empty()} if not found. + */ + Optional findByTokenId(final String tokenId); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/repository/UserRepository.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/repository/UserRepository.java new file mode 100644 index 000000000..df7f490c6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/repository/UserRepository.java @@ -0,0 +1,33 @@ +package com.example.demo.auth.repository; + +import com.example.demo.auth.model.entity.UserEntity; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.Optional; + +/** + * Repository interface for managing {@link UserEntity} objects in Couchbase. + * This interface extends {@link MongoRepository} to provide CRUD operations for the {@link UserEntity}. + * It also includes custom query methods to interact with the data store related to user entities. + */ +public interface UserRepository extends MongoRepository { + + /** + * Checks whether a {@link UserEntity} exists in the database with the specified email. + * This method is used to verify if a user with the given email already exists in the database. + * + * @param email The email address to check for the existence of a user. + * @return {@code true} if a user with the specified email exists, otherwise {@code false}. + */ + boolean existsUserEntityByEmail(final String email); + + /** + * Finds a {@link UserEntity} by its email address. + * This method is used to retrieve a user entity based on the unique email address. + * + * @param email The email address of the user to search for. + * @return An {@link Optional} containing the {@link UserEntity} if found, or {@link Optional#empty()} if not found. + */ + Optional findUserEntityByEmail(final String email); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/security/CustomAuthenticationEntryPoint.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 000000000..405629fb7 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,64 @@ +package com.example.demo.auth.security; + +import com.example.demo.common.model.CustomError; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.text.DateFormat; + +/** + * Custom implementation of the {@link AuthenticationEntryPoint} interface to handle unauthorized access (HTTP 401). + * This class provides a custom entry point for authentication exceptions. When an authentication exception occurs, + * it generates a custom error response with a 401 Unauthorized status code and a specific error message format. + */ +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + } + + /** + * Handles authentication errors by sending a custom error response to the client. + * This method is invoked when an authentication exception occurs. It sets the response content type to + * JSON, status code to 401 (Unauthorized), and writes a custom error response with error details. + * + * @param httpServletRequest The {@link HttpServletRequest} containing the client request data. + * @param httpServletResponse The {@link HttpServletResponse} used to send the error response. + * @param authenticationException The {@link AuthenticationException} that caused the entry point to be triggered. + * @throws IOException If an I/O error occurs while writing the response. + */ + @Override + public void commence(final HttpServletRequest httpServletRequest, + final HttpServletResponse httpServletResponse, + final AuthenticationException authenticationException) throws IOException { + + httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); + httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + + final CustomError customError = CustomError.builder() + .header(CustomError.Header.AUTH_ERROR.getName()) + .httpStatus(HttpStatus.UNAUTHORIZED) + .isSuccess(false) + .build(); + + final String responseBody = OBJECT_MAPPER + .writer(DateFormat.getDateInstance()) + .writeValueAsString(customError); + + httpServletResponse.getOutputStream() + .write(responseBody.getBytes()); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/InvalidTokenService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/InvalidTokenService.java new file mode 100644 index 000000000..637a30202 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/InvalidTokenService.java @@ -0,0 +1,30 @@ +package com.example.demo.auth.service; + +import java.util.Set; + +/** + * Service interface for managing and validating invalidated tokens. + * This service is responsible for invalidating tokens and checking the validity of tokens. + * It provides methods to mark tokens as invalid and to check if a given token has already been invalidated. + */ +public interface InvalidTokenService { + + /** + * Invalidates the tokens with the given token IDs. + * This method marks the tokens as invalid, making them unusable for authentication or authorization. + * It can be used to invalidate tokens that have been revoked or are no longer valid. + * + * @param tokenIds A set of token IDs to invalidate. + */ + void invalidateTokens(final Set tokenIds); + + /** + * Checks if the given token ID is invalidated. + * This method checks whether a specific token has been invalidated and is no longer valid for use. + * It is typically used to validate if a token is still active before allowing access to protected resources. + * + * @param tokenId The token ID to check for invalidity. + */ + void checkForInvalidityOfToken(final String tokenId); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/LoginService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/LoginService.java new file mode 100644 index 000000000..3c4233915 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/LoginService.java @@ -0,0 +1,23 @@ +package com.example.demo.auth.service; + +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.request.LoginRequest; + +/** + * Service interface for handling user login functionality. + * This interface defines the method responsible for logging in a user by validating their credentials + * and generating a token for subsequent authentication requests. + */ +public interface LoginService { + + /** + * Authenticates a user and generates an authentication token. + * This method accepts login credentials, validates the user, and returns a token if the login is successful. + * The token can then be used for authenticated access to protected resources. + * + * @param loginRequest The request containing the login credentials (e.g., username/email and password). + * @return A {@link Token} containing the authentication information, including access and refresh tokens. + */ + Token login(final LoginRequest loginRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/LogoutService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/LogoutService.java new file mode 100644 index 000000000..8f8503ceb --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/LogoutService.java @@ -0,0 +1,21 @@ +package com.example.demo.auth.service; + +import com.example.demo.auth.model.dto.request.TokenInvalidateRequest; + +/** + * Service interface for handling user logout functionality. + * This interface defines the method responsible for logging out a user by invalidating their token. + * It ensures that the user's authentication token is no longer valid, preventing further access to protected resources. + */ +public interface LogoutService { + + /** + * Logs out a user by invalidating their authentication token. + * This method accepts a request containing the token information to be invalidated. Once the token is invalidated, + * it can no longer be used for authentication, ensuring that the user is effectively logged out. + * + * @param tokenInvalidateRequest The request containing the token to be invalidated for logging out the user. + */ + void logout(final TokenInvalidateRequest tokenInvalidateRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/RefreshTokenService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/RefreshTokenService.java new file mode 100644 index 000000000..a87a31b7e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/RefreshTokenService.java @@ -0,0 +1,23 @@ +package com.example.demo.auth.service; + +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.request.TokenRefreshRequest; + +/** + * Service interface for handling the refreshing of authentication tokens. + * This interface defines the method responsible for refreshing the user's authentication token. + * It generates a new access token and possibly a new refresh token, typically used when the old access token has expired. + */ +public interface RefreshTokenService { + + /** + * Refreshes the user's authentication token. + * This method accepts a request containing the refresh token and generates new access and refresh tokens. + * The new tokens can be used for subsequent requests that require authentication. + * + * @param tokenRefreshRequest The request containing the refresh token information to generate new tokens. + * @return A {@link Token} containing the new access and refresh tokens. + */ + Token refreshToken(final TokenRefreshRequest tokenRefreshRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/RegisterService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/RegisterService.java new file mode 100644 index 000000000..8b6011f9e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/RegisterService.java @@ -0,0 +1,23 @@ +package com.example.demo.auth.service; + +import com.example.demo.auth.model.User; +import com.example.demo.auth.model.dto.request.RegisterRequest; + +/** + * Service interface for handling user registration functionality. + * This interface defines the method responsible for registering a new user in the system. + * It processes the registration request, validates the data, and creates a new user entity. + */ +public interface RegisterService { + + /** + * Registers a new user in the system. + * This method accepts a registration request containing the user details (e.g., email, password, etc.). + * It validates the input and creates a new user in the system. + * + * @param registerRequest The request containing the user's registration details (e.g., name, email, password). + * @return A {@link User} object representing the newly created user. + */ + User registerUser(final RegisterRequest registerRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/TokenService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/TokenService.java new file mode 100644 index 000000000..a373ffaf6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/TokenService.java @@ -0,0 +1,96 @@ +package com.example.demo.auth.service; + +import com.example.demo.auth.model.Token; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +import java.util.Map; +import java.util.Set; + +/** + * Service interface for handling JWT token-related operations. + * This interface defines methods for generating, validating, and extracting information from JWT tokens. + * It provides functionality to create tokens, verify their validity, retrieve claims, and authenticate users. + */ +public interface TokenService { + + /** + * Generates a new authentication token based on the provided claims. + * This method creates a new JWT token using the specified claims. + * The claims typically contain user-specific information such as the username, roles, and other custom attributes. + * + * @param claims The claims to be included in the JWT token. + * @return A {@link Token} object containing the generated JWT token. + */ + Token generateToken(final Map claims); + + /** + * Generates a new authentication token based on the provided claims and a refresh token. + * This method creates a new JWT token using the specified claims and includes the provided refresh token as part of the payload. + * It is typically used when refreshing the user's authentication token. + * + * @param claims The claims to be included in the JWT token. + * @param refreshToken The refresh token to include in the generated JWT token. + * @return A {@link Token} object containing the generated JWT token. + */ + Token generateToken(final Map claims, final String refreshToken); + + /** + * Extracts the authentication information from the provided JWT token. + * This method parses the JWT token and returns an {@link UsernamePasswordAuthenticationToken} containing the user authentication information. + * It is typically used to authenticate the user during the request processing. + * + * @param token The JWT token to extract the authentication information from. + * @return A {@link UsernamePasswordAuthenticationToken} containing the extracted authentication information. + */ + UsernamePasswordAuthenticationToken getAuthentication(final String token); + + /** + * Verifies the validity of the provided JWT token. + * This method checks if the provided JWT token is valid, including verifying its signature, expiration, and other factors. + * + * @param jwt The JWT token to verify. + * @throws SecurityException If the token is invalid or cannot be verified. + */ + void verifyAndValidate(final String jwt); + + /** + * Verifies the validity of a set of JWT tokens. + * This method checks the validity of each JWT token in the provided set, + * ensuring that all tokens are valid and have not been tampered with. + * + * @param jwts The set of JWT tokens to verify. + * @throws SecurityException If any of the tokens are invalid or cannot be verified. + */ + void verifyAndValidate(final Set jwts); + + /** + * Extracts the claims from the provided JWT token. + * This method parses the JWT token and retrieves the claims contained in the token. + * Claims typically contain user-specific information. + * + * @param jwt The JWT token to extract claims from. + * @return A {@link Jws} object containing the claims of the token. + */ + Jws getClaims(final String jwt); + + /** + * Retrieves the payload (claims) from the provided JWT token. + * This method extracts the claims (payload) from the JWT token and returns them as a {@link Claims} object. + * + * @param jwt The JWT token to retrieve the payload from. + * @return A {@link Claims} object containing the payload of the token. + */ + Claims getPayload(final String jwt); + + /** + * Retrieves the ID from the provided JWT token. + * This method extracts the unique identifier (ID) from the JWT token, typically representing the user or session. + * + * @param jwt The JWT token to retrieve the ID from. + * @return The unique identifier (ID) from the JWT token. + */ + String getId(final String jwt); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/InvalidTokenServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/InvalidTokenServiceImpl.java new file mode 100644 index 000000000..732d6f1cf --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/InvalidTokenServiceImpl.java @@ -0,0 +1,59 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.TokenAlreadyInvalidatedException; +import com.example.demo.auth.model.entity.InvalidTokenEntity; +import com.example.demo.auth.repository.InvalidTokenRepository; +import com.example.demo.auth.service.InvalidTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Service implementation for managing and validating invalidated tokens. + * This service is responsible for invalidating tokens and checking the validity of tokens. + * It provides methods to mark tokens as invalid and to check if a given token has already been invalidated. + */ +@Service +@RequiredArgsConstructor +public class InvalidTokenServiceImpl implements InvalidTokenService { + + private final InvalidTokenRepository invalidTokenRepository; + + /** + * Invalidates the tokens with the given token IDs. + * This method marks the tokens as invalid, making them unusable for authentication or authorization. + * It can be used to invalidate tokens that have been revoked or are no longer valid. + * + * @param tokenIds A set of token IDs to invalidate. + */ + @Override + public void invalidateTokens(Set tokenIds) { + final Set invalidTokenEntities = tokenIds.stream() + .map(tokenId -> InvalidTokenEntity.builder() + .tokenId(tokenId) + .build() + ) + .collect(Collectors.toSet()); + + invalidTokenRepository.saveAll(invalidTokenEntities); + } + + /** + * Checks if the given token ID is invalidated. + * This method checks whether a specific token has been invalidated and is no longer valid for use. + * It is typically used to validate if a token is still active before allowing access to protected resources. + * + * @param tokenId The token ID to check for invalidity. + */ + @Override + public void checkForInvalidityOfToken(String tokenId) { + final boolean isTokenInvalid = invalidTokenRepository.findByTokenId(tokenId).isPresent(); + + if (isTokenInvalid) { + throw new TokenAlreadyInvalidatedException(tokenId); + } + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/LoginServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/LoginServiceImpl.java new file mode 100644 index 000000000..9a4e340a9 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/LoginServiceImpl.java @@ -0,0 +1,54 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.PasswordNotValidException; +import com.example.demo.auth.exception.UserNotFoundException; +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.request.LoginRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.repository.UserRepository; +import com.example.demo.auth.service.LoginService; +import com.example.demo.auth.service.TokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +/** + * Service implementation for handling user login functionality. + * This interface defines the method responsible for logging in a user by validating their credentials + * and generating a token for subsequent authentication requests. + */ +@Service +@RequiredArgsConstructor +public class LoginServiceImpl implements LoginService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final TokenService tokenService; + + /** + * Authenticates a user and generates an authentication token. + * This method accepts login credentials, validates the user, and returns a token if the login is successful. + * The token can then be used for authenticated access to protected resources. + * + * @param loginRequest The request containing the login credentials (e.g., username/email and password). + * @return A {@link Token} containing the authentication information, including access and refresh tokens. + */ + @Override + public Token login(LoginRequest loginRequest) { + + final UserEntity userEntityFromDB = userRepository + .findUserEntityByEmail(loginRequest.getEmail()) + .orElseThrow( + () -> new UserNotFoundException(loginRequest.getEmail()) + ); + + if (Boolean.FALSE.equals(passwordEncoder.matches( + loginRequest.getPassword(), userEntityFromDB.getPassword()))) { + throw new PasswordNotValidException(); + } + + return tokenService.generateToken(userEntityFromDB.getClaims()); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/LogoutServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/LogoutServiceImpl.java new file mode 100644 index 000000000..6f1df25dd --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/LogoutServiceImpl.java @@ -0,0 +1,58 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.model.dto.request.TokenInvalidateRequest; +import com.example.demo.auth.service.InvalidTokenService; +import com.example.demo.auth.service.LogoutService; +import com.example.demo.auth.service.TokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Set; + +/** + * Service implementation for handling user logout functionality. + * This interface defines the method responsible for logging out a user by invalidating their token. + * It ensures that the user's authentication token is no longer valid, preventing further access to protected resources. + */ +@Service +@RequiredArgsConstructor +public class LogoutServiceImpl implements LogoutService { + + private final TokenService tokenService; + private final InvalidTokenService invalidTokenService; + + /** + * Logs out a user by invalidating their authentication token. + * This method accepts a request containing the token information to be invalidated. Once the token is invalidated, + * it can no longer be used for authentication, ensuring that the user is effectively logged out. + * + * @param tokenInvalidateRequest The request containing the token to be invalidated for logging out the user. + */ + @Override + public void logout(TokenInvalidateRequest tokenInvalidateRequest) { + + tokenService.verifyAndValidate( + Set.of( + tokenInvalidateRequest.getAccessToken(), + tokenInvalidateRequest.getRefreshToken() + ) + ); + + final String accessTokenId = tokenService + .getPayload(tokenInvalidateRequest.getAccessToken()) + .getId(); + + invalidTokenService.checkForInvalidityOfToken(accessTokenId); + + + final String refreshTokenId = tokenService + .getPayload(tokenInvalidateRequest.getRefreshToken()) + .getId(); + + invalidTokenService.checkForInvalidityOfToken(refreshTokenId); + + invalidTokenService.invalidateTokens(Set.of(accessTokenId,refreshTokenId)); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/RefreshTokenServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/RefreshTokenServiceImpl.java new file mode 100644 index 000000000..0f3395eef --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/RefreshTokenServiceImpl.java @@ -0,0 +1,71 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.UserNotFoundException; +import com.example.demo.auth.exception.UserStatusNotValidException; +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.request.TokenRefreshRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.enums.TokenClaims; +import com.example.demo.auth.model.enums.UserStatus; +import com.example.demo.auth.repository.UserRepository; +import com.example.demo.auth.service.RefreshTokenService; +import com.example.demo.auth.service.TokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service implementation for handling the refreshing of authentication tokens. + * This interface defines the method responsible for refreshing the user's authentication token. + * It generates a new access token and possibly a new refresh token, typically used when the old access token has expired. + */ +@Service +@RequiredArgsConstructor +public class RefreshTokenServiceImpl implements RefreshTokenService { + + private final UserRepository userRepository; + private final TokenService tokenService; + + /** + * Refreshes the user's authentication token. + * This method accepts a request containing the refresh token and generates new access and refresh tokens. + * The new tokens can be used for subsequent requests that require authentication. + * + * @param tokenRefreshRequest The request containing the refresh token information to generate new tokens. + * @return A {@link Token} containing the new access and refresh tokens. + */ + @Override + public Token refreshToken(TokenRefreshRequest tokenRefreshRequest) { + tokenService.verifyAndValidate(tokenRefreshRequest.getRefreshToken()); + + final String adminId = tokenService + .getPayload(tokenRefreshRequest.getRefreshToken()) + .get(TokenClaims.USER_ID.getValue()) + .toString(); + + final UserEntity userEntityFromDB = userRepository + .findById(adminId) + .orElseThrow(UserNotFoundException::new); + + this.validateAdminStatus(userEntityFromDB); + + return tokenService.generateToken( + userEntityFromDB.getClaims(), + tokenRefreshRequest.getRefreshToken() + ); + } + + /** + * Validates the user's status to ensure they are active. + * This method checks if the user's status is set to active. + * If the user is not active, it throws a {@link UserStatusNotValidException}. + * + * @param userEntity The {@link UserEntity} to validate. + * @throws UserStatusNotValidException If the user is not in an active status. + */ + private void validateAdminStatus(final UserEntity userEntity) { + if (!(UserStatus.ACTIVE.equals(userEntity.getUserStatus()))) { + throw new UserStatusNotValidException("UserStatus = " + userEntity.getUserStatus()); + } + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/RegisterServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/RegisterServiceImpl.java new file mode 100644 index 000000000..3254cbc47 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/RegisterServiceImpl.java @@ -0,0 +1,58 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.UserAlreadyExistException; +import com.example.demo.auth.model.User; +import com.example.demo.auth.model.dto.request.RegisterRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.mapper.RegisterRequestToUserEntityMapper; +import com.example.demo.auth.model.mapper.UserEntityToUserMapper; +import com.example.demo.auth.repository.UserRepository; +import com.example.demo.auth.service.RegisterService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +/** + * Service implementation for handling user registration functionality. + * This interface defines the method responsible for registering a new user in the system. + * It processes the registration request, validates the data, and creates a new user entity. + */ +@Service +@RequiredArgsConstructor +public class RegisterServiceImpl implements RegisterService { + + private final UserRepository userRepository; + + private final RegisterRequestToUserEntityMapper registerRequestToUserEntityMapper = + RegisterRequestToUserEntityMapper.initialize(); + + private final UserEntityToUserMapper userEntityToUserMapper = UserEntityToUserMapper.initialize(); + + private final PasswordEncoder passwordEncoder; + + /** + * Registers a new user in the system. + * This method accepts a registration request containing the user details (e.g., email, password, etc.). + * It validates the input and creates a new user in the system. + * + * @param registerRequest The request containing the user's registration details (e.g., name, email, password). + * @return A {@link User} object representing the newly created user. + */ + @Override + public User registerUser(final RegisterRequest registerRequest) { + + if (userRepository.existsUserEntityByEmail(registerRequest.getEmail())) { + throw new UserAlreadyExistException("The email is already used for another user : " + registerRequest.getEmail()); + } + + final UserEntity userEntityToBeSaved = registerRequestToUserEntityMapper.mapForSaving(registerRequest); + + userEntityToBeSaved.setPassword(passwordEncoder.encode(registerRequest.getPassword())); + + final UserEntity savedUserEntity = userRepository.save(userEntityToBeSaved); + + return userEntityToUserMapper.map(savedUserEntity); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/TokenServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/TokenServiceImpl.java new file mode 100644 index 000000000..c5bee482f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/service/impl/TokenServiceImpl.java @@ -0,0 +1,253 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.config.TokenConfigurationParameter; +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.enums.ConfigurationParameter; +import com.example.demo.auth.model.enums.TokenClaims; +import com.example.demo.auth.model.enums.TokenType; +import com.example.demo.auth.model.enums.UserType; +import com.example.demo.auth.service.InvalidTokenService; +import com.example.demo.auth.service.TokenService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * Service implementation for handling JWT token-related operations. + * This interface defines methods for generating, validating, and extracting information from JWT tokens. + * It provides functionality to create tokens, verify their validity, retrieve claims, and authenticate users. + */ +@Service +@RequiredArgsConstructor +public class TokenServiceImpl implements TokenService { + + private final TokenConfigurationParameter tokenConfigurationParameter; + private final InvalidTokenService invalidTokenService; + + /** + * Generates a new authentication token based on the provided claims. + * This method creates a new JWT token using the specified claims. + * The claims typically contain user-specific information such as the username, roles, and other custom attributes. + * + * @param claims The claims to be included in the JWT token. + * @return A {@link Token} object containing the generated JWT token. + */ + @Override + public Token generateToken(Map claims) { + + final long currentTimeMillis = System.currentTimeMillis(); + + final Date tokenIssuedAt = new Date(currentTimeMillis); + + final Date accessTokenExpiresAt = DateUtils.addMinutes( + new Date(currentTimeMillis), + tokenConfigurationParameter.getAccessTokenExpireMinute() + ); + + final String accessToken = Jwts.builder() + .header() + .type(TokenType.BEARER.getValue()) + .and() + .id(UUID.randomUUID().toString()) + .issuer(ConfigurationParameter.ISSUER.getDefaultValue()) + .issuedAt(tokenIssuedAt) + .expiration(accessTokenExpiresAt) + .signWith(tokenConfigurationParameter.getPrivateKey()) + .claims(claims) + .compact(); + + final Date refreshTokenExpiresAt = DateUtils.addDays( + new Date(currentTimeMillis), + tokenConfigurationParameter.getRefreshTokenExpireDay() + ); + + final String refreshToken = Jwts.builder() + .header() + .type(TokenType.BEARER.getValue()) + .and() + .id(UUID.randomUUID().toString()) + .issuer(tokenConfigurationParameter.getIssuer()) + .issuedAt(tokenIssuedAt) + .expiration(refreshTokenExpiresAt) + .signWith(tokenConfigurationParameter.getPrivateKey()) + .claim(TokenClaims.USER_ID.getValue(), claims.get(TokenClaims.USER_ID.getValue())) + .compact(); + + return Token.builder() + .accessToken(accessToken) + .accessTokenExpiresAt(accessTokenExpiresAt.toInstant().getEpochSecond()) + .refreshToken(refreshToken) + .build(); + } + + /** + * Generates a new authentication token based on the provided claims and a refresh token. + * This method creates a new JWT token using the specified claims and includes the provided refresh token as part of the payload. + * It is typically used when refreshing the user's authentication token. + * + * @param claims The claims to be included in the JWT token. + * @param refreshToken The refresh token to include in the generated JWT token. + * @return A {@link Token} object containing the generated JWT token. + */ + @Override + public Token generateToken(Map claims, String refreshToken) { + + final long currentTimeMillis = System.currentTimeMillis(); + + final String refreshTokenId = this.getId(refreshToken); + + invalidTokenService.checkForInvalidityOfToken(refreshTokenId); + + final Date accessTokenIssuedAt = new Date(currentTimeMillis); + + final Date accessTokenExpiresAt = DateUtils.addMinutes( + new Date(currentTimeMillis), + tokenConfigurationParameter.getAccessTokenExpireMinute() + ); + + final String accessToken = Jwts.builder() + .header() + .type(TokenType.BEARER.getValue()) + .and() + .id(UUID.randomUUID().toString()) + .issuer(tokenConfigurationParameter.getIssuer()) + .issuedAt(accessTokenIssuedAt) + .expiration(accessTokenExpiresAt) + .signWith(tokenConfigurationParameter.getPrivateKey()) + .claims(claims) + .compact(); + + return Token.builder() + .accessToken(accessToken) + .accessTokenExpiresAt(accessTokenExpiresAt.toInstant().getEpochSecond()) + .refreshToken(refreshToken) + .build(); + } + + /** + * Extracts the authentication information from the provided JWT token. + * This method parses the JWT token and returns an {@link UsernamePasswordAuthenticationToken} containing the user authentication information. + * It is typically used to authenticate the user during the request processing. + * + * @param token The JWT token to extract the authentication information from. + * @return A {@link UsernamePasswordAuthenticationToken} containing the extracted authentication information. + */ + @Override + public UsernamePasswordAuthenticationToken getAuthentication(String token) { + + final Jws claimsJws = Jwts.parser() + .verifyWith(tokenConfigurationParameter.getPublicKey()) + .build() + .parseSignedClaims(token); + + final JwsHeader jwsHeader = claimsJws.getHeader(); + final Claims payload = claimsJws.getPayload(); + + final Jwt jwt = new org.springframework.security.oauth2.jwt.Jwt( + token, + payload.getIssuedAt().toInstant(), + payload.getExpiration().toInstant(), + Map.of( + TokenClaims.TYP.getValue(), jwsHeader.getType(), + TokenClaims.ALGORITHM.getValue(), jwsHeader.getAlgorithm() + ), + payload + ); + + + final UserType userType = UserType.valueOf(payload.get(TokenClaims.USER_TYPE.getValue()).toString()); + + final List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(userType.name())); + + return UsernamePasswordAuthenticationToken + .authenticated(jwt, null, authorities); + } + + /** + * Verifies the validity of the provided JWT token. + * This method checks if the provided JWT token is valid, including verifying its signature, expiration, and other factors. + * + * @param jwt The JWT token to verify. + * @throws SecurityException If the token is invalid or cannot be verified. + */ + @Override + public void verifyAndValidate(String jwt) { + Jwts.parser() + .verifyWith(tokenConfigurationParameter.getPublicKey()) + .build() + .parseSignedClaims(jwt); + } + + /** + * Verifies the validity of a set of JWT tokens. + * This method checks the validity of each JWT token in the provided set, + * ensuring that all tokens are valid and have not been tampered with. + * + * @param jwts The set of JWT tokens to verify. + * @throws SecurityException If any of the tokens are invalid or cannot be verified. + */ + @Override + public void verifyAndValidate(Set jwts) { + jwts.forEach(this::verifyAndValidate); + } + + /** + * Extracts the claims from the provided JWT token. + * This method parses the JWT token and retrieves the claims contained in the token. + * Claims typically contain user-specific information. + * + * @param jwt The JWT token to extract claims from. + * @return A {@link Jws} object containing the claims of the token. + */ + @Override + public Jws getClaims(String jwt) { + return Jwts.parser() + .verifyWith(tokenConfigurationParameter.getPublicKey()) + .build() + .parseSignedClaims(jwt); + } + + /** + * Retrieves the payload (claims) from the provided JWT token. + * This method extracts the claims (payload) from the JWT token and returns them as a {@link Claims} object. + * + * @param jwt The JWT token to retrieve the payload from. + * @return A {@link Claims} object containing the payload of the token. + */ + @Override + public Claims getPayload(String jwt) { + return Jwts.parser() + .verifyWith(tokenConfigurationParameter.getPublicKey()) + .build() + .parseSignedClaims(jwt) + .getPayload(); + } + + /** + * Retrieves the ID from the provided JWT token. + * This method extracts the unique identifier (ID) from the JWT token, typically representing the user or session. + * + * @param jwt The JWT token to retrieve the ID from. + * @return The unique identifier (ID) from the JWT token. + */ + @Override + public String getId(String jwt) { + return Jwts.parser() + .verifyWith(tokenConfigurationParameter.getPublicKey()) + .build() + .parseSignedClaims(jwt) + .getPayload() + .getId(); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/utils/KeyConverter.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/utils/KeyConverter.java new file mode 100644 index 000000000..f134440f6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/auth/utils/KeyConverter.java @@ -0,0 +1,66 @@ +package com.example.demo.auth.utils; + +import lombok.experimental.UtilityClass; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; + +import java.io.IOException; +import java.io.StringReader; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Utility class for converting PEM-encoded public and private keys to {@link PublicKey} and {@link PrivateKey} objects. + * This class provides methods to convert PEM-encoded key strings to their respective {@link PublicKey} and {@link PrivateKey} objects + * using the BouncyCastle library. + */ +@UtilityClass +public class KeyConverter { + + /** + * Converts a PEM-encoded public key string to a {@link PublicKey} object. + * This method parses a PEM-encoded public key string, extracts the public key information, and returns the corresponding + * {@link PublicKey} object. + * + * @param publicPemKey The PEM-encoded public key string. + * @return The corresponding {@link PublicKey} object. + * @throws RuntimeException if there is an error reading or parsing the PEM key. + */ + public PublicKey convertPublicKey(final String publicPemKey) { + + final StringReader keyReader = new StringReader(publicPemKey); + try { + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo + .getInstance(new PEMParser(keyReader).readObject()); + return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + + } + + /** + * Converts a PEM-encoded private key string to a {@link PrivateKey} object. + * This method parses a PEM-encoded private key string, extracts the private key information, and returns the corresponding + * {@link PrivateKey} object. + * + * @param privatePemKey The PEM-encoded private key string. + * @return The corresponding {@link PrivateKey} object. + * @throws RuntimeException if there is an error reading or parsing the PEM key. + */ + public PrivateKey convertPrivateKey(final String privatePemKey) { + + StringReader keyReader = new StringReader(privatePemKey); + try { + PrivateKeyInfo privateKeyInfo = PrivateKeyInfo + .getInstance(new PEMParser(keyReader).readObject()); + return new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/audit/BaseEntityListener.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/audit/BaseEntityListener.java new file mode 100644 index 000000000..91059ed4d --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/audit/BaseEntityListener.java @@ -0,0 +1,83 @@ +package com.example.demo.common.audit; + +import com.example.demo.auth.model.enums.TokenClaims; +import com.example.demo.common.model.entity.BaseEntity; +import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + +/** + * Listener for MongoDB lifecycle events to manage auditing fields in {@link BaseEntity}. + */ +@Component +public class BaseEntityListener extends AbstractMongoEventListener { + + /** + * Sets auditing fields before converting the entity to a MongoDB document. + * + * @param event the event triggered before entity conversion. + */ + @Override + public void onBeforeConvert(BeforeConvertEvent event) { + BaseEntity entity = event.getSource(); + + // Set ID for entities that have it + setEntityIdIfNeeded(entity); + + if (entity.getCreatedAt() == null) { + entity.setCreatedAt(LocalDateTime.now()); + entity.setCreatedBy(getCurrentUser()); + } + + // Always update 'updatedAt' and 'updatedBy' + entity.setUpdatedAt(LocalDateTime.now()); + entity.setUpdatedBy(getCurrentUser()); + + } + + + /** + * Checks if the entity has an 'id' field and sets it to a new UUID if it is null or empty. + * + * @param entity the entity to check and modify + */ + private void setEntityIdIfNeeded(BaseEntity entity) { + try { + // Use reflection to check if the entity has an 'id' field + Field idField = entity.getClass().getDeclaredField("id"); + idField.setAccessible(true); // Make the field accessible + + // If the field is null or empty, set it to a new UUID + Object idValue = idField.get(entity); + if (idValue == null || idValue.toString().isEmpty()) { + idField.set(entity, UUID.randomUUID().toString()); + } + } catch (NoSuchFieldException | IllegalAccessException e) { + // If no 'id' field is found, or it can't be accessed, ignore (entity may not have an 'id' field) + } + } + + /** + * Retrieves the email of the currently authenticated user from the security context. + * If no authenticated user is found, it returns "anonymousUser". + * + * @return the email of the current user or "anonymousUser" if not authenticated + */ + private String getCurrentUser() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getPrincipal) + .filter(user -> !"anonymousUser".equals(user)) + .map(Jwt.class::cast) + .map(jwt -> jwt.getClaim(TokenClaims.USER_EMAIL.getValue()).toString()) + .orElse("anonymousUser"); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/config/MongoConfig.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/config/MongoConfig.java new file mode 100644 index 000000000..b11f3a03e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/config/MongoConfig.java @@ -0,0 +1,13 @@ +package com.example.demo.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; + +/** + * Configuration class to enable auditing for MongoDB entities. + */ +@Configuration +@EnableMongoAuditing +public class MongoConfig { + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/config/OpenApiConfig.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/config/OpenApiConfig.java new file mode 100644 index 000000000..e1a807ccf --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/config/OpenApiConfig.java @@ -0,0 +1,36 @@ +package com.example.demo.common.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; + +/** + * Configuration class for setting up OpenAPI documentation for the application. + * This class configures the metadata and security settings for the API documentation using OpenAPI 3.0 annotations. + */ +@OpenAPIDefinition( + info = @Info( + contact = @Contact( + name = "Sercan Noyan Germiyanoğlu", + url = "https://github.com/Rapter1990/flightsearchapi" + ), + description = "Case Study - Flight Search Api" + + " (Java 21, Spring Boot, Mongodb, JUnit, Spring Security, JWT, Docker, Kubernetes, Prometheus, Grafana, Github Actions (CI/CD) ) ", + title = "flightsearchapi", + version = "1.0.0" + ) +) +@SecurityScheme( + name = "bearerAuth", + description = "JWT Token", + scheme = "bearer", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + in = SecuritySchemeIn.HEADER +) +public class OpenApiConfig { + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/exception/GlobalExceptionHandler.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..eabf3848a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,310 @@ +package com.example.demo.common.exception; + +import com.example.demo.auth.exception.*; +import com.example.demo.common.model.CustomError; +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.exception.AirportNotFoundException; +import jakarta.validation.ConstraintViolationException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Global exception handler for handling various types of exceptions across the application. + * This class provides centralized error handling for the entire application and returns custom error responses. + */ +@RestControllerAdvice +class GlobalExceptionHandler { + + /** + * Handles MethodArgumentNotValidException, which is thrown when method arguments are not valid. + * The response contains details about the validation errors for each field. + * + * @param ex The MethodArgumentNotValidException that is thrown. + * @return ResponseEntity containing the custom error message and validation details. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException ex) { + + List subErrors = new ArrayList<>(); + + ex.getBindingResult().getAllErrors().forEach(error -> { + // Determine the field name, checking if it's a FieldError or not + String fieldName = error instanceof FieldError + ? ((FieldError) error).getField() + : error.getObjectName(); // Fallback to object name if not FieldError + + String message = error.getDefaultMessage(); + subErrors.add( + CustomError.CustomSubError.builder() + .field(fieldName) + .message(message) + .build() + ); + }); + + CustomError customError = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message("Validation failed") + .subErrors(subErrors) + .build(); + + return new ResponseEntity<>(customError, HttpStatus.BAD_REQUEST); + + } + + /** + * Handles ConstraintViolationException, which is thrown when there are constraint violations in method parameters. + * The response contains details about the constraint violations, including the field, message, invalid value, and type. + * + * @param constraintViolationException The ConstraintViolationException that is thrown. + * @return ResponseEntity containing the custom error message and constraint violation details. + */ + @ExceptionHandler(ConstraintViolationException.class) + protected ResponseEntity handlePathVariableErrors(final ConstraintViolationException constraintViolationException) { + + List subErrors = new ArrayList<>(); + constraintViolationException.getConstraintViolations() + .forEach(constraintViolation -> + subErrors.add( + CustomError.CustomSubError.builder() + .message(constraintViolation.getMessage()) + .field(StringUtils.substringAfterLast(constraintViolation.getPropertyPath().toString(), ".")) + .value(constraintViolation.getInvalidValue() != null ? constraintViolation.getInvalidValue().toString() : null) + .type(constraintViolation.getInvalidValue().getClass().getSimpleName()) + .build() + ) + ); + + CustomError customError = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message("Constraint violation") + .subErrors(subErrors) + .build(); + + return new ResponseEntity<>(customError, HttpStatus.BAD_REQUEST); + + } + + /** + * Handles general RuntimeException. It responds with an error message and a 404 NOT_FOUND status. + * + * @param runtimeException The RuntimeException that is thrown. + * @return ResponseEntity containing the custom error message. + */ + @ExceptionHandler(RuntimeException.class) + protected ResponseEntity handleRuntimeException(final RuntimeException runtimeException) { + CustomError customError = CustomError.builder() + .httpStatus(HttpStatus.NOT_FOUND) + .header(CustomError.Header.API_ERROR.getName()) + .message(runtimeException.getMessage()) + .build(); + + return new ResponseEntity<>(customError, HttpStatus.NOT_FOUND); + } + + /** + * Handles AccessDeniedException, which is thrown when a user does not have permission to access a resource. + * The response contains an access denied message and a 403 FORBIDDEN status. + * + * @param accessDeniedException The AccessDeniedException that is thrown. + * @return ResponseEntity containing the custom error message and access denied status. + */ + @ExceptionHandler(AccessDeniedException.class) + protected ResponseEntity handleAccessDeniedException(final AccessDeniedException accessDeniedException) { + CustomError customError = CustomError.builder() + .httpStatus(HttpStatus.FORBIDDEN) + .header(CustomError.Header.AUTH_ERROR.getName()) + .message(accessDeniedException.getMessage()) + .build(); + + return new ResponseEntity<>(customError, HttpStatus.FORBIDDEN); + } + + /** + * Handles PasswordNotValidException, which is thrown when a provided password does not meet validation criteria. + * The response contains the validation error message and a 400 BAD_REQUEST status. + * + * @param ex The PasswordNotValidException that is thrown. + * @return ResponseEntity containing the custom error message. + */ + @ExceptionHandler(PasswordNotValidException.class) + protected ResponseEntity handlePasswordNotValidException(final PasswordNotValidException ex) { + + CustomError error = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + /** + * Handles RoleNotFoundException, which is thrown when a role cannot be found in the system. + * The response contains the error message and a 404 NOT_FOUND status. + * + * @param ex The RoleNotFoundException that is thrown. + * @return ResponseEntity containing the custom error message. + */ + @ExceptionHandler(RoleNotFoundException.class) + protected ResponseEntity handleRoleNotFoundException(final RoleNotFoundException ex) { + + CustomError error = CustomError.builder() + .time(LocalDateTime.now()) + .httpStatus(HttpStatus.NOT_FOUND) + .header(CustomError.Header.NOT_FOUND.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + /** + * Handles TokenAlreadyInvalidatedException, which is thrown when a token has already been invalidated. + * The response contains the error message and a 400 BAD_REQUEST status. + * + * @param ex The TokenAlreadyInvalidatedException that is thrown. + * @return ResponseEntity containing the custom error message. + */ + @ExceptionHandler(TokenAlreadyInvalidatedException.class) + protected ResponseEntity handleTokenAlreadyInvalidatedException(final TokenAlreadyInvalidatedException ex) { + CustomError error = CustomError.builder() + .time(LocalDateTime.now()) + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + /** + * Handles UserAlreadyExistException, which is thrown when a user already exists in the system. + * The response contains the error message and a 409 CONFLICT status. + * + * @param ex The UserAlreadyExistException that is thrown. + * @return ResponseEntity containing the custom error message. + */ + @ExceptionHandler(UserAlreadyExistException.class) + protected ResponseEntity handleUserAlreadyExistException(final UserAlreadyExistException ex) { + CustomError error = CustomError.builder() + .httpStatus(HttpStatus.CONFLICT) + .header(CustomError.Header.ALREADY_EXIST.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + return new ResponseEntity<>(error, HttpStatus.CONFLICT); + } + + /** + * Handles UserNotFoundException, which is thrown when a user cannot be found in the system. + * The response contains the error message and a 404 NOT_FOUND status. + * + * @param ex The UserNotFoundException that is thrown. + * @return ResponseEntity containing the custom error message. + */ + @ExceptionHandler(UserNotFoundException.class) + protected ResponseEntity handleUserNotFoundException(final UserNotFoundException ex) { + CustomError error = CustomError.builder() + .httpStatus(HttpStatus.NOT_FOUND) + .header(CustomError.Header.NOT_FOUND.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + /** + * Handles UserStatusNotValidException, which is thrown when a user's status is invalid. + * The response contains the error message and a 400 BAD_REQUEST status. + * + * @param ex The UserStatusNotValidException that is thrown. + * @return ResponseEntity containing the custom error message. + */ + @ExceptionHandler(UserStatusNotValidException.class) + protected ResponseEntity handleUserStatusNotValidException(final UserStatusNotValidException ex) { + CustomError error = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + /** + * Handles UnauthorizedAttemptException, which is thrown when an unauthorized access attempt is detected. + * The response contains an "Unauthorized" message and a 401 UNAUTHORIZED status. + * + * @param ex The UnauthorizedAttemptException that was thrown. + * @return ResponseEntity containing the custom error message and UNAUTHORIZED status. + */ + @ExceptionHandler(UnAuthorizeAttemptException.class) + protected ResponseEntity handleUnAuthorizeAttempt(final UnAuthorizeAttemptException ex){ + + CustomError customError = CustomError.builder() + .httpStatus(HttpStatus.UNAUTHORIZED) + .header(CustomError.Header.AUTH_ERROR.getName()) + .message(ex.getMessage()) + .build(); + + return new ResponseEntity<>(customError, HttpStatus.UNAUTHORIZED); + } + + /** + * Handles AirportNotFoundException, which is thrown when a requested airport cannot be found. + * The response contains the error message and a 404 NOT_FOUND status. + * + * @param ex The AirportNotFoundException that was thrown. + * @return ResponseEntity containing the custom error message and NOT_FOUND status. + */ + @ExceptionHandler(AirportNotFoundException.class) + protected ResponseEntity handleAirportNotFoundException(final AirportNotFoundException ex) { + + CustomError error = CustomError.builder() + .time(LocalDateTime.now()) + .httpStatus(HttpStatus.NOT_FOUND) + .header(CustomError.Header.NOT_FOUND.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + + } + + /** + * Handles AirportNameAlreadyExistException, which is thrown when an airport with the same name already exists. + * The response contains the error message and a custom status code provided by the exception. + * + * @param ex The AirportNameAlreadyExistException that was thrown. + * @return ResponseEntity containing the custom error message and the custom status code. + */ + @ExceptionHandler(AirportNameAlreadyExistException.class) + protected ResponseEntity handleAirportNameAlreadyExistException(final AirportNameAlreadyExistException ex) { + + CustomError error = CustomError.builder() + .time(LocalDateTime.now()) + .httpStatus(AirportNameAlreadyExistException.STATUS) + .header(CustomError.Header.BAD_REQUEST.getName()) + .message(ex.getMessage()) + .isSuccess(false) + .build(); + + return new ResponseEntity<>(error, AirportNameAlreadyExistException.STATUS); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/BaseDomainModel.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/BaseDomainModel.java new file mode 100644 index 000000000..a7beb9d6a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/BaseDomainModel.java @@ -0,0 +1,36 @@ +package com.example.demo.common.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.time.LocalDateTime; + +/** + * A base class for domain models that include common auditing fields such as + * creation and update timestamps, as well as the user responsible for those changes. + * This class serves as a foundation for other domain models, providing essential fields + * to track the creation and modification metadata, which is crucial for auditing purposes. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public abstract class BaseDomainModel { + + @Field(name = "createdAt") + private LocalDateTime createdAt; + + @Field(name = "createdBy") + private String createdBy; + + @Field(name = "updatedAt") + private LocalDateTime updatedAt; + + @Field(name = "updatedBy") + private String updatedBy; +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomError.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomError.java new file mode 100644 index 000000000..b3854a485 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomError.java @@ -0,0 +1,86 @@ +package com.example.demo.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * A class representing a custom error response. This class is used to structure error responses + * in a standardized format, including HTTP status, error messages, time of occurrence, and optional + * sub-errors to provide more detailed information about what went wrong. + * This class can be used in API responses to provide error details when an operation fails. + */ +@Getter +@Builder +public class CustomError { + + @Builder.Default + private LocalDateTime time = LocalDateTime.now(); + + private HttpStatus httpStatus; + + private String header; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String message; + + @Builder.Default + private final Boolean isSuccess = false; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private List subErrors; + + /** + * A static class representing a sub-error, which contains additional information about specific + * aspects of the error, such as a specific field and its associated value. + */ + @Getter + @Builder + public static class CustomSubError { + + private String message; + + private String field; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Object value; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String type; + + } + + /** + * An enum representing predefined headers for different types of errors. + * These headers help categorize the error and provide a quick understanding of its context. + */ + @Getter + @RequiredArgsConstructor + public enum Header { + + API_ERROR("API ERROR"), + + ALREADY_EXIST("ALREADY EXIST"), + + NOT_FOUND("NOT EXIST"), + + VALIDATION_ERROR("VALIDATION ERROR"), + + DATABASE_ERROR("DATABASE ERROR"), + + PROCESS_ERROR("PROCESS ERROR"), + + AUTH_ERROR("AUTH ERROR"), + + BAD_REQUEST("BAD_REQUEST"); + + private final String name; + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomPage.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomPage.java new file mode 100644 index 000000000..f384817a5 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomPage.java @@ -0,0 +1,55 @@ +package com.example.demo.common.model; + +import lombok.*; +import org.springframework.data.domain.Page; + +import java.util.List; + +/** + * A class representing a paginated response for a list of items. + * This class is used to wrap a list of items along with pagination details such as the page number, + * page size, total number of elements, and total number of pages. + * This is typically used in APIs to return large sets of data in chunks, while providing metadata + * about the pagination (e.g., for implementing pagination in front-end applications). + * + * @param The type of items contained in the page (e.g., `User`, `Product`). + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomPage { + private List content; + + private Integer pageNumber; + + private Integer pageSize; + + private Long totalElementCount; + + private Integer totalPageCount; + + /** + * A utility method to create a `CustomPage` instance from a list of domain models and a Spring Data `Page` object. + *

+ * This method extracts relevant pagination information from the provided `Page` object and combines it + * with the list of domain models to construct a `CustomPage`. + * + * @param The type of the content in the new `CustomPage`. + * @param The type of the content in the provided `Page` (usually the same as `C`, but could differ). + * @param domainModels A list of domain models to include in the page's content. + * @param page The Spring Data `Page` object containing pagination metadata. + * @return A new `CustomPage` instance containing the domain models and pagination details. + */ + public static CustomPage of(final List domainModels, final Page page) { + return CustomPage.builder() + .content(domainModels) + .pageNumber(page.getNumber() + 1) + .pageSize(page.getSize()) + .totalPageCount(page.getTotalPages()) + .totalElementCount(page.getTotalElements()) + .build(); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomPaging.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomPaging.java new file mode 100644 index 000000000..103541894 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/CustomPaging.java @@ -0,0 +1,43 @@ +package com.example.demo.common.model; + +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +/** + * A class representing the pagination information for a request. + * This class holds the page number and page size, which are used to control the data returned in paginated responses. + * It is typically used in APIs to handle pagination for large datasets, where only a subset of data is returned at a time. + * This class ensures that the page number and page size provided by the user meet the minimum constraints. + * The page number is expected to be 1-based, but internally it is converted to 0-based indexing. + * + * @see #getPageNumber() for how the page number is adjusted. + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +public class CustomPaging { + + @Min(value = 1, message = "Page number must be bigger than 0") + private Integer pageNumber; + + @Min(value = 1, message = "Page size must be bigger than 0") + private Integer pageSize; + + /** + * Returns the 0-based page number for internal use in pagination. + * This method adjusts the page number by subtracting 1 from the provided 1-based index. + * For example, if the user provides page 1, this method will return 0 to match Spring Data's 0-based page indexing. + * + * @return The adjusted 0-based page number. + */ + public Integer getPageNumber() { + return pageNumber - 1; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/request/CustomPagingRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/request/CustomPagingRequest.java new file mode 100644 index 000000000..6cf7c3e16 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/request/CustomPagingRequest.java @@ -0,0 +1,38 @@ +package com.example.demo.common.model.dto.request; + +import com.example.demo.common.model.CustomPaging; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +/** + * Abstract class that represents a custom paging request. + * This class provides pagination details through a `CustomPaging` object + * and includes a method to convert the pagination information into a `Pageable` object. + * It is intended to be extended by other classes that require paging functionality. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +public abstract class CustomPagingRequest { + + private CustomPaging pagination; + + /** + * Converts the `CustomPaging` object into a `Pageable` object. + * The `Pageable` object is used by Spring Data to handle pagination in queries. + * + * @return A `Pageable` object containing the page number and page size derived from the `pagination` field. + */ + public Pageable toPageable() { + return PageRequest.of( + Math.toIntExact(pagination.getPageNumber()), + Math.toIntExact(pagination.getPageSize()) + ); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/response/CustomPagingResponse.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/response/CustomPagingResponse.java new file mode 100644 index 000000000..232639f3b --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/response/CustomPagingResponse.java @@ -0,0 +1,49 @@ +package com.example.demo.common.model.dto.response; + +import com.example.demo.common.model.CustomPage; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +/** + * Represents a custom paging response that holds paginated data along with + * pagination details such as the current page number, page size, total element count, + * and total page count. + * This class is used to encapsulate the response for a paginated query. + * + * @param The type of the elements in the paginated response. + */ +@Getter +@Builder +public class CustomPagingResponse { + + private List content; + + private Integer pageNumber; + + private Integer pageSize; + + private Long totalElementCount; + + private Integer totalPageCount; + + /** + * Builder class for {@link CustomPagingResponse}. It provides a custom builder method + * to create a {@link CustomPagingResponse} from a {@link CustomPage}. + * + * @param The type of the elements in the paginated response. + */ + public static class CustomPagingResponseBuilder { + + public CustomPagingResponseBuilder of(final CustomPage customPage) { + return CustomPagingResponse.builder() + .pageNumber(customPage.getPageNumber()) + .pageSize(customPage.getPageSize()) + .totalElementCount(customPage.getTotalElementCount()) + .totalPageCount(customPage.getTotalPageCount()); + } + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/response/CustomResponse.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/response/CustomResponse.java new file mode 100644 index 000000000..b03c80a03 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/dto/response/CustomResponse.java @@ -0,0 +1,56 @@ +package com.example.demo.common.model.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +/** + * A generic class that represents a standard response structure. + * It includes metadata about the response such as the status, success flag, + * response data, and timestamp. + * + * @param The type of the response data. + */ +@Getter +@Builder +public class CustomResponse { + + @Builder.Default + private LocalDateTime time = LocalDateTime.now(); + + private HttpStatus httpStatus; + + private Boolean isSuccess; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private T response; + + /** + * A predefined successful response with no data. It represents a successful + * operation with a HTTP status of 200 (OK). + */ + public static final CustomResponse SUCCESS = CustomResponse.builder() + .httpStatus(HttpStatus.OK) + .isSuccess(true) + .build(); + + + /** + * Creates a successful response with the provided data. + * + * @param response The response data to include in the response body. + * @param The type of the response data. + * @return A {@link CustomResponse} instance with the provided response data. + */ + public static CustomResponse successOf(final T response) { + return CustomResponse.builder() + .httpStatus(HttpStatus.OK) + .isSuccess(true) + .response(response) + .build(); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/entity/BaseEntity.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/entity/BaseEntity.java new file mode 100644 index 000000000..07cc2584b --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/entity/BaseEntity.java @@ -0,0 +1,39 @@ +package com.example.demo.common.model.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.time.LocalDateTime; + +/** + * A base entity class that provides common fields for tracking the creation + * and update metadata of an entity. This class is intended to be extended by other + * entity classes to include audit information such as creation and update timestamps + * and user details. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class BaseEntity { + + + @Field(name = "createdAt") + private LocalDateTime createdAt; + + @Field(name = "createdBy") + private String createdBy; + + @Field(name = "updatedAt") + private LocalDateTime updatedAt; + + @Field(name = "updatedBy") + private String updatedBy; + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/mapper/BaseMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/mapper/BaseMapper.java new file mode 100644 index 000000000..5d44af113 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/common/model/mapper/BaseMapper.java @@ -0,0 +1,32 @@ +package com.example.demo.common.model.mapper; + +import java.util.Collection; +import java.util.List; + +/** + * A generic interface for mapping between two types, {@code S} (source) and {@code T} (target). + * This interface provides methods to map a single object or a collection of objects from type {@code S} + * to type {@code T}. + * + * @param the source type + * @param the target type + */ +public interface BaseMapper { + + /** + * Maps a single object of type {@code S} to an object of type {@code T}. + * + * @param source the source object to be mapped + * @return the mapped target object of type {@code T} + */ + T map(S source); + + /** + * Maps a collection of objects of type {@code S} to a list of objects of type {@code T}. + * + * @param sources the collection of source objects to be mapped + * @return a list of mapped target objects of type {@code T} + */ + List map(Collection sources); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/AirportController.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/AirportController.java new file mode 100644 index 000000000..8a0129c85 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/AirportController.java @@ -0,0 +1,183 @@ +package com.example.demo.flight.controller; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.common.model.dto.response.CustomResponse; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.AirportPagingRequest; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.model.dto.response.airport.AirportResponse; +import com.example.demo.flight.model.mapper.airport.AirportToAirportResponseMapper; +import com.example.demo.flight.model.mapper.airport.CustomPageAirportToCustomPagingAirportResponseMapper; +import com.example.demo.flight.service.airport.AirportService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.hibernate.validator.constraints.UUID; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * RestController for managing airports. + * Provides endpoints for creating, retrieving, updating, and deleting airport information. + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("api/v1/airports") +@Validated +@Tag(name = "Airport Management", + description = "Endpoints for managing airports including creation, retrieval, updating, and deletion.") +public class AirportController { + + private final AirportService airportService; + private final AirportToAirportResponseMapper airportToAirportResponseMapper = AirportToAirportResponseMapper.initialize(); + + private final CustomPageAirportToCustomPagingAirportResponseMapper customPageAirportToCustomPagingAirportResponseMapper + = CustomPageAirportToCustomPagingAirportResponseMapper.initialize(); + + + /** + * Creates a new airport and saves it to the database. + * + * @param createAirportRequest the request body containing the details of the airport to be created. + * It must include a valid airport name and city name. + * @return a {@link CustomResponse} containing the ID of the newly created airport. + */ + @Operation( + summary = "Create a new airport", + description = "Creates a new airport and saves it to the database. Accessible by ADMIN only.", + responses = { + @ApiResponse(responseCode = "200", description = "Airport successfully created"), + @ApiResponse(responseCode = "400", description = "Invalid airport details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden") + } + ) + @PostMapping + @PreAuthorize("hasAuthority('ADMIN')") + public CustomResponse createAirport(@RequestBody @Valid final CreateAirportRequest createAirportRequest){ + final Airport savedAirport = airportService.createAirport(createAirportRequest); + + return CustomResponse.successOf(savedAirport.getId()); + } + + /** + * Retrieves an airport by its ID. + * + * @param id the ID of the airport to be retrieved. + * @return a response containing the airport details. + */ + @Operation( + summary = "Get airport by ID", + description = "Retrieves an airport by its ID. Accessible by both ADMIN and USER roles.", + responses = { + @ApiResponse(responseCode = "200", description = "Airport successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Invalid airport details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Airport not found") + } + ) + @GetMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ADMIN','USER')") + public CustomResponse getAirportById(@PathVariable @Valid @UUID final String id){ + final Airport airport = airportService.getAirportById(id); + + final AirportResponse response = airportToAirportResponseMapper.map(airport); + + return CustomResponse.successOf(response); + } + + /** + * Retrieves a paginated list of airports. + * + * @param request the request body containing pagination and sorting information. + * @return a paginated response containing a list of airports. + */ + @Operation( + summary = "Get all airports", + description = "Retrieves a paginated list of airports. Accessible by both ADMIN and USER roles.", + responses = { + @ApiResponse(responseCode = "200", description = "Airports successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Invalid airports details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Airport not found") + } + ) + @GetMapping + @PreAuthorize("hasAnyAuthority('ADMIN','USER')") + public CustomResponse> getAllAirports(@RequestBody @Valid final AirportPagingRequest request){ + final CustomPage airportCustomPage= airportService.getAllAirports(request); + + final CustomPagingResponse response = customPageAirportToCustomPagingAirportResponseMapper + .toPagingResponse(airportCustomPage); + + return CustomResponse.successOf(response); + } + + /** + * Updates an existing airport by its ID. + * + * @param id the ID of the airport to be updated. + * @param updateAirportRequest the request body containing the updated airport details. + * @return a response containing the updated airport details. + */ + @Operation( + summary = "Update an airport", + description = "Updates an existing airport by its ID. Accessible by ADMIN only.", + responses = { + @ApiResponse(responseCode = "200", description = "Airport successfully updated"), + @ApiResponse(responseCode = "400", description = "Invalid update details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Airport not found") + } + ) + @PutMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ADMIN')") + public CustomResponse updateAirportById( + @PathVariable @Valid @UUID final String id, + @RequestBody @Valid final UpdateAirportRequest updateAirportRequest){ + + final Airport airport = airportService.updateAirportById(id, updateAirportRequest); + + final AirportResponse response = airportToAirportResponseMapper.map(airport); + + return CustomResponse.successOf(response); + + } + + /** + * Deletes an airport by its ID. + * + * @param id the ID of the airport to be deleted. + * @return a response containing a success message. + */ + @Operation( + summary = "Delete an airport", + description = "Deletes an airport by its ID. Accessible by ADMIN only.", + responses = { + @ApiResponse(responseCode = "200", description = "Airport successfully deleted"), + @ApiResponse(responseCode = "400", description = "Invalid update details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Airport not found") + } + ) + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ADMIN')") + public CustomResponse deleteAirportById(@PathVariable @Valid @UUID final String id){ + + airportService.deleteAirportById(id); + + return CustomResponse.successOf("Airport with id "+ id + "is deleted"); + + } + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/FlightController.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/FlightController.java new file mode 100644 index 000000000..3d6cb3886 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/FlightController.java @@ -0,0 +1,183 @@ +package com.example.demo.flight.controller; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.common.model.dto.response.CustomResponse; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.airport.AirportPagingRequest; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import com.example.demo.flight.model.mapper.flight.CustomPageFlightToCustomPagingFlightResponseMapper; +import com.example.demo.flight.model.mapper.flight.FlightToFlightResponseMapper; +import com.example.demo.flight.service.flight.FlightService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.hibernate.validator.constraints.UUID; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * RestController for managing flights. + * Provides endpoints for creating, retrieving, updating, and deleting flight information. + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("api/v1/flights") +@Validated +@Tag(name = "Flight Management", + description = "Endpoints for managing flights including creation, retrieval, updating, and deletion.") +public class FlightController { + + private final FlightService flightService; + + private final FlightToFlightResponseMapper flightToFlightResponseMapper = FlightToFlightResponseMapper.initialize(); + + private final CustomPageFlightToCustomPagingFlightResponseMapper customPageFlightToCustomPagingFlightResponseMapper + = CustomPageFlightToCustomPagingFlightResponseMapper.initialize(); + + + /** + * Creates a new flight and saves it to the database. + * + * @param createFlightRequest the request body containing details of the flight to be created. + * @return a {@link CustomResponse} containing the ID of the newly created flight. + */ + @Operation( + summary = "Create a new flight", + description = "Creates a new flight and saves it to the database. Accessible by ADMIN only.", + responses = { + @ApiResponse(responseCode = "200", description = "Flight successfully created"), + @ApiResponse(responseCode = "400", description = "Invalid flight details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden") + } + ) + @PostMapping + @PreAuthorize("hasAuthority('ADMIN')") + public CustomResponse createFlight(@RequestBody @Valid final CreateFlightRequest createFlightRequest){ + final Flight savedFlight = flightService.createFlight(createFlightRequest); + + return CustomResponse.successOf(savedFlight.getId()); + } + + /** + * Retrieves a flight by its ID. + * + * @param id the ID of the flight to be retrieved. + * @return a response containing the flight details. + */ + @Operation( + summary = "Get flight by ID", + description = "Retrieves a flight by its ID. Accessible by both ADMIN and USER roles.", + responses = { + @ApiResponse(responseCode = "200", description = "Flight successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Invalid flight details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Airport not found") + } + ) + @GetMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ADMIN','USER')") + public CustomResponse getFlightById(@PathVariable @Valid @UUID final String id){ + + final Flight flight = flightService.getFlightById(id); + + final FlightResponse response = flightToFlightResponseMapper.map(flight); + + return CustomResponse.successOf(response); + + } + + /** + * Retrieves a paginated list of flights. + * + * @param request the request body containing pagination and sorting information. + * @return a paginated response containing a list of flights. + */ + @Operation( + summary = "Get all flights", + description = "Retrieves a paginated list of flights. Accessible by both ADMIN and USER roles.", + responses = { + @ApiResponse(responseCode = "200", description = "Flights successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Invalid flights details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Flight not found") + } + ) + @GetMapping + @PreAuthorize("hasAnyAuthority('ADMIN','USER')") + public CustomResponse> getAllFlights(@RequestBody @Valid final AirportPagingRequest request){ + final CustomPage flightCustomPage= flightService.getAllFlights(request); + + final CustomPagingResponse response = customPageFlightToCustomPagingFlightResponseMapper + .toPagingResponse(flightCustomPage); + + return CustomResponse.successOf(response); + } + + /** + * Updates an existing flight by its ID. + * + * @param id the ID of the flight to be updated. + * @param updateFlightRequest the request body containing the updated flight details. + * @return a response containing the updated flight details. + */ + @Operation( + summary = "Update an flight", + description = "Updates an existing flight by its ID. Accessible by ADMIN only.", + responses = { + @ApiResponse(responseCode = "200", description = "Flight successfully updated"), + @ApiResponse(responseCode = "400", description = "Invalid update details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Flight not found") + } + ) + @PutMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ADMIN')") + public CustomResponse updateFlightById( + @PathVariable @Valid @UUID final String id, + @RequestBody @Valid final UpdateFlightRequest updateFlightRequest) { + + final Flight flight = flightService.updateFlightById(id, updateFlightRequest); + + final FlightResponse response = flightToFlightResponseMapper.map(flight); + + return CustomResponse.successOf(response); + } + + /** + * Deletes a flight by its ID. + * + * @param id the ID of the flight to be deleted. + * @return a response containing a success message. + */ + @Operation( + summary = "Delete an flight", + description = "Deletes a flight by its ID. Accessible by ADMIN only.", + responses = { + @ApiResponse(responseCode = "200", description = "Flight successfully deleted"), + @ApiResponse(responseCode = "400", description = "Invalid update details provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden"), + @ApiResponse(responseCode = "404", description = "Flight not found") + } + ) + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ADMIN')") + public CustomResponse deleteAirportById(@PathVariable @Valid @UUID final String id){ + + flightService.deleteFlightById(id); + + return CustomResponse.successOf("Flight with id "+ id + "is deleted"); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/SearchFlightController.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/SearchFlightController.java new file mode 100644 index 000000000..6c49cd85d --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/controller/SearchFlightController.java @@ -0,0 +1,69 @@ +package com.example.demo.flight.controller; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.common.model.dto.response.CustomResponse; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.SearchFlightRequest; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import com.example.demo.flight.model.mapper.flight.CustomPageFlightToCustomPagingFlightResponseMapper; +import com.example.demo.flight.service.flight.SearchFlightService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * RestController for searching flights based on given criteria. + * Provides an endpoint for searching flights with filters such as departure, arrival airports, and date. + */ +@RestController +@RequestMapping("api/v1/flights/search") +@RequiredArgsConstructor +@Validated +@Tag(name = "Search Flight", description = "Endpoints for searching flights based on given criteria.") +public class SearchFlightController { + + private final SearchFlightService searchFlightService; + + private final CustomPageFlightToCustomPagingFlightResponseMapper customPageFlightToCustomPagingFlightResponseMapper = + CustomPageFlightToCustomPagingFlightResponseMapper.initialize(); + + /** + * Searches for flights based on given criteria. + * + * @param request the search criteria and pagination details. + * @return a paginated response containing a list of matching flights. + */ + @Operation( + summary = "Search for flights", + description = "Searches for flights based on departure, arrival airports, departure date, and optionally return date. Accessible by ADMIN and USER roles.", + responses = { + @ApiResponse(responseCode = "200", description = "Flights successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Invalid search criteria provided"), + @ApiResponse(responseCode = "401", description = "Unauthorized, authentication is required"), + @ApiResponse(responseCode = "403", description = "Access forbidden") + } + ) + @PostMapping + @PreAuthorize("hasAnyAuthority('ADMIN','USER')") + public CustomResponse> searchFlights( + @RequestBody @Valid SearchFlightRequest request) { + + CustomPage flightPage = searchFlightService.searchFlights(request); + + CustomPagingResponse response = + customPageFlightToCustomPagingFlightResponseMapper.toPagingResponse(flightPage); + + return CustomResponse.successOf(response); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/AirportNameAlreadyExistException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/AirportNameAlreadyExistException.java new file mode 100644 index 000000000..e5a79023f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/AirportNameAlreadyExistException.java @@ -0,0 +1,42 @@ +package com.example.demo.flight.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when an airport with the same name already exists in the system. + * This exception is typically used when attempting to create or update an airport with a name + * that already exists, violating the uniqueness constraint. + */ +public class AirportNameAlreadyExistException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -3495060754159297663L; + + public static final HttpStatus STATUS = HttpStatus.BAD_REQUEST; + + private static final String DEFAULT_MESSAGE = """ + Airport with this name already exist + """; + + /** + * Constructs a new AirportNameAlreadyExistException with the default error message. + * This constructor is used when an airport with the same name already exists, and no additional details are provided. + */ + public AirportNameAlreadyExistException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new AirportNameAlreadyExistException with a custom error message. + * This constructor is used when an airport with the same name already exists, and an additional message is provided + * to further explain the context. + * + * @param message the custom message to be appended to the default error message. + */ + public AirportNameAlreadyExistException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/AirportNotFoundException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/AirportNotFoundException.java new file mode 100644 index 000000000..d70daf220 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/AirportNotFoundException.java @@ -0,0 +1,40 @@ +package com.example.demo.flight.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when an airport is not found in the system. + * This exception is typically used when an airport cannot be located based on the provided identifier. + */ +public class AirportNotFoundException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 7222351960801029610L; + + public static final HttpStatus STATUS = HttpStatus.BAD_REQUEST; + + private static final String DEFAULT_MESSAGE = """ + Airport not found! + """; + + /** + * Constructs a new AirportNotFoundException with the default error message. + * This constructor is used when an airport is not found, and no additional details are provided. + */ + public AirportNotFoundException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new AirportNotFoundException with a custom error message. + * This constructor is used when an airport is not found, and an additional message is provided to further explain the context. + * + * @param message the custom message to be appended to the default error message. + */ + public AirportNotFoundException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/FlightNotFoundException.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/FlightNotFoundException.java new file mode 100644 index 000000000..f19bd333f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/exception/FlightNotFoundException.java @@ -0,0 +1,41 @@ +package com.example.demo.flight.exception; + +import org.springframework.http.HttpStatus; + +import java.io.Serial; + +/** + * Custom exception thrown when a flight is not found in the system. + * This exception is typically used when a flight cannot be located based on the provided identifier. + */ +public class FlightNotFoundException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -1042600589031710869L; + + public static final HttpStatus STATUS = HttpStatus.BAD_REQUEST; + + private static final String DEFAULT_MESSAGE = """ + Flight not found! + """; + + /** + * Constructs a new FlightNotFoundException with the default error message. + * This constructor is used when a flight is not found, and no additional details are provided. + */ + public FlightNotFoundException() { + super(DEFAULT_MESSAGE); + } + + /** + * Constructs a new FlightNotFoundException with a custom error message. + * This constructor is used when a flight is not found, and an additional message is provided to further explain the context. + * + * @param message the custom message to be appended to the default error message. + */ + public FlightNotFoundException(final String message) { + super(DEFAULT_MESSAGE + " " + message); + } + +} + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/job/FlightDataLoader.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/job/FlightDataLoader.java new file mode 100644 index 000000000..b66470004 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/job/FlightDataLoader.java @@ -0,0 +1,98 @@ +package com.example.demo.flight.job; + +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.repository.FlightRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * FlightDataLoader is responsible for initializing dummy airport and flight data + * in the database. It creates and saves sample airport entities and flight entities. + */ +@Component +@RequiredArgsConstructor +public class FlightDataLoader { + + private final FlightRepository flightRepository; + private final AirportRepository airportRepository; + + /** + * Loads sample airport and flight data into the database. + * Creates airport entities and associated flights with dynamic schedules. + */ + public void loadFlightDumpyData(){ + + AirportEntity airport1 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Los Angeles International Airport") + .cityName("Los Angeles") + .build(); + + AirportEntity airport2 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("John F. Kennedy International Airport") + .cityName("New York") + .build(); + + AirportEntity airport3 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Chicago O'Hare International Airport") + .cityName("Chicago") + .build(); + + airportRepository.saveAll(List.of(airport1, airport2, airport3)); + + List airports = airportRepository.findAll(); + + // Define dynamic departure times + LocalDateTime departureFlight1 = LocalDateTime.now().plusDays(1) + .withHour(10).withMinute(0); // Tomorrow at 10:00 AM + LocalDateTime arrivalFlight1 = departureFlight1.plusHours(4); // Arrives 4 hours later (2 PM) + + LocalDateTime departureFlight2 = departureFlight1.plusDays(1) + .withHour(9).withMinute(0); // Return flight next day at 9:00 AM + LocalDateTime arrivalFlight2 = departureFlight2.plusHours(4); // Arrives back at 1:00 PM + + LocalDateTime departureFlight3 = departureFlight1.plusDays(2) + .withHour(15).withMinute(30); // New York → Chicago (Next day 3:30 PM) + LocalDateTime arrivalFlight3 = departureFlight3.plusHours(3); // Arrives 3 hours later (6:30 PM) + + // Create dummy flights + FlightEntity flight1 = FlightEntity.builder() + .id(UUID.randomUUID().toString()) + .fromAirport(airports.get(0)) // Los Angeles + .toAirport(airports.get(1)) // New York + .departureTime(departureFlight1) + .arrivalTime(arrivalFlight1) + .price(250.0) + .build(); + + FlightEntity flight2 = FlightEntity.builder() + .id(UUID.randomUUID().toString()) + .fromAirport(airports.get(1)) // New York + .toAirport(airports.get(0)) // Los Angeles (Return flight) + .departureTime(departureFlight2) + .arrivalTime(arrivalFlight2) + .price(260.0) + .build(); + + FlightEntity flight3 = FlightEntity.builder() + .id(UUID.randomUUID().toString()) + .fromAirport(airports.get(1)) // New York + .toAirport(airports.get(2)) // Chicago + .departureTime(departureFlight3) + .arrivalTime(arrivalFlight3) + .price(180.0) + .build(); + + flightRepository.saveAll(List.of(flight1, flight2, flight3)); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/job/ScheduledJob.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/job/ScheduledJob.java new file mode 100644 index 000000000..25ae1a676 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/job/ScheduledJob.java @@ -0,0 +1,27 @@ +package com.example.demo.flight.job; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * ScheduledJob is a scheduled task that runs at midnight every day + * to load mock flight data into the database. + */ +@Component +@RequiredArgsConstructor +public class ScheduledJob { + + private final FlightDataLoader flightDataLoader; + + /** + * Executes the flight data loading job at midnight (00:00:00) daily. + * This method calls {@link FlightDataLoader#loadFlightDumpyData()} + * to populate the database with mock flight data. + */ + @Scheduled(cron = "0 0 0 * * ?") + public void loadMockFlightsToDatabase() { + flightDataLoader.loadFlightDumpyData(); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/Airport.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/Airport.java new file mode 100644 index 000000000..cecaac2e2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/Airport.java @@ -0,0 +1,26 @@ +package com.example.demo.flight.model; + +import com.example.demo.common.model.BaseDomainModel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +/** + * Represents an airport in the system, with properties such as its identifier, name, and city. + * Extends {@link BaseDomainModel} to inherit common domain model attributes and behavior. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@SuperBuilder +public class Airport extends BaseDomainModel { + + private String id; + + private String name; + + private String cityName; +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/Flight.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/Flight.java new file mode 100644 index 000000000..960575e4f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/Flight.java @@ -0,0 +1,31 @@ +package com.example.demo.flight.model; + +import com.example.demo.common.model.BaseDomainModel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +/** + * Represents an airport in the system with basic properties such as an identifier and a name. + * This class extends {@link BaseDomainModel} and provides functionality to model a task + * in the application. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@SuperBuilder +public class Flight extends BaseDomainModel { + + private String id; + private Airport fromAirport; + private Airport toAirport; + private LocalDateTime departureTime; + private LocalDateTime arrivalTime; + private Double price; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/AirportPagingRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/AirportPagingRequest.java new file mode 100644 index 000000000..2cc169c1e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/AirportPagingRequest.java @@ -0,0 +1,20 @@ +package com.example.demo.flight.model.dto.request.airport; + +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +/** + * Request class used for paginated airport retrieval. + * Inherits from the base class {@link CustomPagingRequest}, which provides pagination details such as page number and size. + * This class is used to request a paginated list of airports. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +public class AirportPagingRequest extends CustomPagingRequest { + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/CreateAirportRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/CreateAirportRequest.java new file mode 100644 index 000000000..fcab50e5b --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/CreateAirportRequest.java @@ -0,0 +1,24 @@ +package com.example.demo.flight.model.dto.request.airport; + +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +/** + * Represents a request to create a new airport. + * This class contains fields for the airport's name and the city where it is located. + * Both fields are mandatory and cannot be left blank. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class CreateAirportRequest { + + @NotBlank(message = "Name field cannot be empty") + private String name; + + @NotBlank(message = "City Name field cannot be empty") + private String cityName; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/UpdateAirportRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/UpdateAirportRequest.java new file mode 100644 index 000000000..1203940d5 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/airport/UpdateAirportRequest.java @@ -0,0 +1,23 @@ +package com.example.demo.flight.model.dto.request.airport; + +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +/** + * Request class used to update an airport. + * Contains fields 'name' and 'cityName' which represent airport's fields to be updated. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class UpdateAirportRequest { + + @NotBlank(message = "name field cannot be empty") + private String name; + + @NotBlank(message = "cityName field cannot be empty") + private String cityName; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/CreateFlightRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/CreateFlightRequest.java new file mode 100644 index 000000000..9b835c351 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/CreateFlightRequest.java @@ -0,0 +1,40 @@ +package com.example.demo.flight.model.dto.request.flight; + +import com.example.demo.flight.utils.annotations.ValidArrivalTime; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * Represents a request to create a new flight. + * This class contains fields for the flight's origin, destination, departure time, arrival time, and price. + * All fields are mandatory and must meet the specified validation constraints. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@ValidArrivalTime +public class CreateFlightRequest { + + @NotBlank(message = "From airport ID field cannot be empty") + private String fromAirportId; + + @NotBlank(message = "To airport ID field cannot be empty") + private String toAirportId; + + @NotNull(message = "Departure time field cannot be null") + private LocalDateTime departureTime; + + @NotNull(message = "Arrival time field cannot be null") + private LocalDateTime arrivalTime; + + @NotNull(message = "Price field cannot be null") + @Positive(message = "Price must be greater than zero") + private Double price; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/FlightPagingRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/FlightPagingRequest.java new file mode 100644 index 000000000..d37c6c8bb --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/FlightPagingRequest.java @@ -0,0 +1,20 @@ +package com.example.demo.flight.model.dto.request.flight; + +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +/** + * Request class used for paginated flight retrieval. + * Inherits from the base class {@link CustomPagingRequest}, which provides pagination details such as page number and size. + * This class is used to request a paginated list of flights. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +public class FlightPagingRequest extends CustomPagingRequest { + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/SearchFlightRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/SearchFlightRequest.java new file mode 100644 index 000000000..b79ef3882 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/SearchFlightRequest.java @@ -0,0 +1,45 @@ +package com.example.demo.flight.model.dto.request.flight; + +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.utils.CustomLocalDateTimeDeserializer; +import com.example.demo.flight.utils.annotations.ValidArrivalTime; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +/** + * Request class used for searching flights with specific criteria. + * Inherits from the base class {@link CustomPagingRequest}, which provides pagination details such as page number and size. + * This class is used to request a paginated list of flights based on specific search parameters. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@ValidArrivalTime +public class SearchFlightRequest extends CustomPagingRequest { + + @NotNull + private String fromAirportId; + + @NotNull + private String toAirportId; + + @NotNull + @JsonDeserialize(using = CustomLocalDateTimeDeserializer.class) + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDateTime departureTime; + + @JsonDeserialize(using = CustomLocalDateTimeDeserializer.class) + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDateTime arrivalTime; // Nullable for one-way flights + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/UpdateFlightRequest.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/UpdateFlightRequest.java new file mode 100644 index 000000000..595e19e02 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/request/flight/UpdateFlightRequest.java @@ -0,0 +1,40 @@ +package com.example.demo.flight.model.dto.request.flight; + +import com.example.demo.flight.utils.annotations.ValidArrivalTime; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * Represents a request to update an existing flight. + * This class contains fields for the flight's updated origin, destination, departure time, arrival time, and price. + * All fields are mandatory and must meet the specified validation constraints. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@ValidArrivalTime +public class UpdateFlightRequest { + + @NotBlank(message = "From airport ID field cannot be empty") + private String fromAirportId; + + @NotBlank(message = "To airport ID field cannot be empty") + private String toAirportId; + + @NotNull(message = "Departure time field cannot be null") + private LocalDateTime departureTime; + + @NotNull(message = "Arrival time field cannot be null") + private LocalDateTime arrivalTime; + + @NotNull(message = "Price field cannot be null") + @Positive(message = "Price must be greater than zero") + private Double price; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/response/airport/AirportResponse.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/response/airport/AirportResponse.java new file mode 100644 index 000000000..70436e756 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/response/airport/AirportResponse.java @@ -0,0 +1,19 @@ +package com.example.demo.flight.model.dto.response.airport; + +import lombok.*; + +/** + * Response class representing an airport. + * This class is used to send information about a airport, including its ID,name and city name. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class AirportResponse { + + private String id; + private String name; + private String cityName; +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/response/flight/FlightResponse.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/response/flight/FlightResponse.java new file mode 100644 index 000000000..48ccf8428 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/dto/response/flight/FlightResponse.java @@ -0,0 +1,31 @@ +package com.example.demo.flight.model.dto.response.flight; + +import com.example.demo.flight.model.dto.response.airport.AirportResponse; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * Response class representing a flight. + * This class provides information about a flight, including: + * - The unique flight ID. + * - The departure and arrival airports. + * - The departure and arrival times. + * - The ticket price. + * + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class FlightResponse { + + private String id; + private AirportResponse fromAirport; + private AirportResponse toAirport; + private LocalDateTime departureTime; + private LocalDateTime arrivalTime; + private Double price; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/entity/AirportEntity.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/entity/AirportEntity.java new file mode 100644 index 000000000..234a3885f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/entity/AirportEntity.java @@ -0,0 +1,35 @@ +package com.example.demo.flight.model.entity; + +import com.example.demo.common.model.entity.BaseEntity; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +/** + * Represents an airport entity stored in the MongoDB {@code airport-collection}. + * Contains airport details such as {@code name} and {@code cityName}. + * Extends {@link BaseEntity} for common timestamp fields. + */ +@Getter +@Setter +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "airport-collection") +public class AirportEntity extends BaseEntity { + + @Id + @Indexed(unique = true) + private String id; + + @Field(name = "AIRPORT_NAME") + private String name; + + @Field(name = "CITY_NAME") + private String cityName; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/entity/FlightEntity.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/entity/FlightEntity.java new file mode 100644 index 000000000..2a42aa0c4 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/entity/FlightEntity.java @@ -0,0 +1,50 @@ +package com.example.demo.flight.model.entity; + +import com.example.demo.common.model.entity.BaseEntity; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.time.LocalDateTime; + +/** + * Represents a flight entity stored in the MongoDB {@code flight-collection}. + * Contains details about the flight, including departure and destination airports, + * departure time, and price. + * Extends {@link BaseEntity} for common timestamp fields. + */ +@Getter +@Setter +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "flight-collection") +public class FlightEntity extends BaseEntity { + + @Id + @Indexed(unique = true) + private String id; + + @DBRef + @Field(name = "FROM_AIRPORT") + private AirportEntity fromAirport; + + @DBRef + @Field(name = "TO_AIRPORT") + private AirportEntity toAirport; + + @Field(name = "DEPARTURE_TIME") + private LocalDateTime departureTime; + + @Field(name = "ARRIVAL_TIME") + private LocalDateTime arrivalTime; + + @Field(name = "PRICE") + private Double price; + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/AirportEntityToAirportMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/AirportEntityToAirportMapper.java new file mode 100644 index 000000000..a10fda638 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/AirportEntityToAirportMapper.java @@ -0,0 +1,27 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.entity.AirportEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + + +/** + * Mapper interface for converting a {@link AirportEntity} to a {@link Airport}. + * This interface extends the {@link BaseMapper} interface, enabling automatic mapping + * between {@link AirportEntity} and {@link Airport} objects. + */ +@Mapper +public interface AirportEntityToAirportMapper extends BaseMapper { + + /** + * Initializes and returns an instance of the {@link AirportEntityToAirportMapper}. + * + * @return an instance of the mapper + */ + static AirportEntityToAirportMapper initialize() { + return Mappers.getMapper(AirportEntityToAirportMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/AirportToAirportResponseMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/AirportToAirportResponseMapper.java new file mode 100644 index 000000000..f1f5a6fd2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/AirportToAirportResponseMapper.java @@ -0,0 +1,28 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.response.airport.AirportResponse; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting a {@link Airport} to a {@link AirportResponse}. + * This interface extends the {@link BaseMapper} interface, enabling automatic mapping + * between {@link Airport} and {@link AirportResponse} objects. + */ +@Mapper +public interface AirportToAirportResponseMapper extends BaseMapper { + + /** + * Initializes and returns an instance of the {@link AirportToAirportResponseMapper}. + * This method is used to obtain a mapper instance for converting a {@link Airport} + * object into a {@link AirportResponse} object. + * + * @return an instance of the {@link AirportToAirportResponseMapper} mapper + */ + static AirportToAirportResponseMapper initialize() { + return Mappers.getMapper(AirportToAirportResponseMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/CreateAirportRequestToAirportEntityMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/CreateAirportRequestToAirportEntityMapper.java new file mode 100644 index 000000000..7b6222bf3 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/CreateAirportRequestToAirportEntityMapper.java @@ -0,0 +1,43 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting a {@link CreateAirportRequest} to a {@link AirportEntity}. + * This interface extends the {@link BaseMapper} interface, allowing automatic mapping + * between the {@link CreateAirportRequest} and {@link AirportEntity} objects. + */ +@Mapper +public interface CreateAirportRequestToAirportEntityMapper extends BaseMapper { + + /** + * Converts a {@link CreateAirportRequest} to a {@link AirportEntity}. + * This method is used for saving an airport, mapping only the necessary fields from the + * {@link CreateAirportRequest} to the {@link AirportEntity}. + * + * @param request the {@link CreateAirportRequest} to be mapped + * @return the resulting {@link AirportEntity} containing the mapped fields + */ + @Named("mapForSaving") + default AirportEntity mapForSaving(CreateAirportRequest request){ + return AirportEntity.builder() + .name(request.getName()) + .cityName(request.getCityName()) + .build(); + } + + /** + * Initializes and returns an instance of the {@link CreateAirportRequestToAirportEntityMapper}. + * + * @return an instance of the mapper + */ + static CreateAirportRequestToAirportEntityMapper initialize(){ + return Mappers.getMapper(CreateAirportRequestToAirportEntityMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/CustomPageAirportToCustomPagingAirportResponseMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/CustomPageAirportToCustomPagingAirportResponseMapper.java new file mode 100644 index 000000000..d0f181cc6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/CustomPageAirportToCustomPagingAirportResponseMapper.java @@ -0,0 +1,71 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.response.airport.AirportResponse; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Mapper interface that converts a {@link CustomPage} of {@link Airport} entities to a {@link CustomPagingResponse} of {@link AirportResponse}. + * It leverages MapStruct for automatic mapping between domain and DTO objects. + */ +@Mapper +public interface CustomPageAirportToCustomPagingAirportResponseMapper { + + AirportToAirportResponseMapper airportToAirportResponseMapper = Mappers.getMapper(AirportToAirportResponseMapper.class); + + /** + * Converts a {@link CustomPage} of {@link Airport} entities to a {@link CustomPagingResponse} containing {@link AirportResponse} DTOs. + * + * @param airportPage the {@link CustomPage} containing a list of {@link Airport} entities + * @return a {@link CustomPagingResponse} with the mapped {@link AirportResponse} list, or {@code null} if {@code airportPage} is {@code null} + */ + default CustomPagingResponse toPagingResponse(CustomPage airportPage) { + + if (airportPage == null) { + return null; + } + + return CustomPagingResponse.builder() + .content(toAirportResponseList(airportPage.getContent())) + .totalElementCount(airportPage.getTotalElementCount()) + .totalPageCount(airportPage.getTotalPageCount()) + .pageNumber(airportPage.getPageNumber()) + .pageSize(airportPage.getPageSize()) + .build(); + + } + + /** + * Converts a list of {@link Airport} entities to a list of {@link AirportResponse} DTOs. + * + * @param airports the list of {@link Airport} entities + * @return a list of {@link AirportResponse} DTOs, or {@code null} if {@code airports} is {@code null} + */ + default List toAirportResponseList(List airports) { + + if (airports == null) { + return null; + } + + return airports.stream() + .map(airportToAirportResponseMapper::map) + .collect(Collectors.toList()); + + } + + /** + * Initializes and returns an instance of the {@link CustomPageAirportToCustomPagingAirportResponseMapper}. + * + * @return an instance of the mapper + */ + static CustomPageAirportToCustomPagingAirportResponseMapper initialize() { + return Mappers.getMapper(CustomPageAirportToCustomPagingAirportResponseMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/ListAirportEntityToListAirportMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/ListAirportEntityToListAirportMapper.java new file mode 100644 index 000000000..46fba51a7 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/ListAirportEntityToListAirportMapper.java @@ -0,0 +1,47 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.entity.AirportEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Mapper interface that converts a list of {@link AirportEntity} objects to a list of {@link Airport} objects. + * It leverages MapStruct for automatic mapping between entity and domain objects. + */ +@Mapper +public interface ListAirportEntityToListAirportMapper { + + AirportEntityToAirportMapper airportEntityToAirportMapper = Mappers.getMapper(AirportEntityToAirportMapper.class); + + /** + * Converts a list of {@link AirportEntity} objects to a list of {@link Airport} objects. + * + * @param airportEntities the list of {@link AirportEntity} objects to be converted + * @return a list of {@link Airport} objects, or {@code null} if {@code airportEntities} is {@code null} + */ + default List toAirportList(List airportEntities) { + + if (airportEntities == null) { + return null; + } + + return airportEntities.stream() + .map(airportEntityToAirportMapper::map) + .collect(Collectors.toList()); + + } + + /** + * Initializes and returns an instance of the {@link ListAirportEntityToListAirportMapper}. + * + * @return an instance of the mapper + */ + static ListAirportEntityToListAirportMapper initialize() { + return Mappers.getMapper(ListAirportEntityToListAirportMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/UpdateAirportRequestToAirportEntityMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/UpdateAirportRequestToAirportEntityMapper.java new file mode 100644 index 000000000..b65bd6924 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/airport/UpdateAirportRequestToAirportEntityMapper.java @@ -0,0 +1,45 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting and updating a {@link AirportEntity} using data from an {@link UpdateAirportRequest}. + * This interface extends the {@link BaseMapper} interface, enabling automatic mapping + * between {@link UpdateAirportRequest} and {@link AirportEntity}. + */ +@Mapper +public interface UpdateAirportRequestToAirportEntityMapper extends BaseMapper { + + /** + * Updates the fields of an existing {@link AirportEntity} with data from an {@link UpdateAirportRequest}. + * This method is used to map the fields from the request to the entity, specifically + * updating the name of the task. + * + * @param airportEntity the {@link AirportEntity} to be updated + * @param updateTaskRequest the {@link UpdateAirportRequest} containing the new data + */ + @Named("mapForUpdate") + default void updateAirportMapper(final AirportEntity airportEntity, final UpdateAirportRequest updateTaskRequest){ + + airportEntity.setName(updateTaskRequest.getName()); + airportEntity.setCityName(updateTaskRequest.getCityName()); + + } + + /** + * Initializes and returns an instance of the {@link UpdateAirportRequestToAirportEntityMapper}. + * This method is used to obtain a mapper instance for mapping an {@link UpdateAirportRequest} + * to a {@link AirportEntity}, including updating an existing entity. + * + * @return an instance of the {@link UpdateAirportRequestToAirportEntityMapper} mapper + */ + static UpdateAirportRequestToAirportEntityMapper initialize(){ + return Mappers.getMapper(UpdateAirportRequestToAirportEntityMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/CreateFlightRequestToFlightEntityMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/CreateFlightRequestToFlightEntityMapper.java new file mode 100644 index 000000000..aadfe706d --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/CreateFlightRequestToFlightEntityMapper.java @@ -0,0 +1,51 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.airport.CreateAirportRequestToAirportEntityMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting a {@link CreateFlightRequest} to a {@link FlightEntity}. + * This interface extends the {@link BaseMapper} interface, allowing automatic mapping + * between the {@link CreateFlightRequest} and {@link FlightEntity} objects. + */ +@Mapper +public interface CreateFlightRequestToFlightEntityMapper extends BaseMapper { + + /** + * Converts a {@link CreateFlightRequest} to a {@link FlightEntity}. + * This method is used for saving a flight, mapping only the necessary fields from the + * {@link CreateFlightRequest} to the {@link FlightEntity}. + * + * @param request the {@link CreateFlightRequest} to be mapped + * @param fromAirport the from airport entity + * @param toAirport the to airport entity + * @return the resulting {@link FlightEntity} containing the mapped fields + */ + @Named("mapForSaving") + default FlightEntity mapForSaving(CreateFlightRequest request, AirportEntity fromAirport, AirportEntity toAirport) { + return FlightEntity.builder() + .fromAirport(fromAirport) + .toAirport(toAirport) + .departureTime(request.getDepartureTime()) + .arrivalTime(request.getArrivalTime()) + .price(request.getPrice()) + .build(); + } + + /** + * Initializes and returns an instance of the {@link CreateAirportRequestToAirportEntityMapper}. + * + * @return an instance of the mapper + */ + static CreateFlightRequestToFlightEntityMapper initialize(){ + return Mappers.getMapper(CreateFlightRequestToFlightEntityMapper.class); + } + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/CustomPageFlightToCustomPagingFlightResponseMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/CustomPageFlightToCustomPagingFlightResponseMapper.java new file mode 100644 index 000000000..303324e45 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/CustomPageFlightToCustomPagingFlightResponseMapper.java @@ -0,0 +1,72 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Mapper interface that converts a {@link CustomPage} of {@link Flight} entities to a {@link CustomPagingResponse} of {@link FlightResponse}. + * It leverages MapStruct for automatic mapping between domain and DTO objects. + */ +@Mapper +public interface CustomPageFlightToCustomPagingFlightResponseMapper { + + FlightToFlightResponseMapper flighttoFlightResponseMapper = Mappers.getMapper(FlightToFlightResponseMapper.class); + + /** + * Converts a {@link CustomPage} of {@link Flight} entities to a {@link CustomPagingResponse} containing {@link FlightResponse} DTOs. + * + * @param flightPage the {@link CustomPage} containing a list of {@link Flight} entities + * @return a {@link CustomPagingResponse} with the mapped {@link FlightResponse} list, or {@code null} if {@code flightPage} is {@code null} + */ + default CustomPagingResponse toPagingResponse(CustomPage flightPage) { + + if (flightPage == null) { + return null; + } + + return CustomPagingResponse.builder() + .content(toFlightResponseList(flightPage.getContent())) + .totalElementCount(flightPage.getTotalElementCount()) + .totalPageCount(flightPage.getTotalPageCount()) + .pageNumber(flightPage.getPageNumber()) + .pageSize(flightPage.getPageSize()) + .build(); + + } + + /** + * Converts a list of {@link Flight} entities to a list of {@link FlightResponse} DTOs. + * + * @param flights the list of {@link Flight} entities + * @return a list of {@link FlightResponse} DTOs, or {@code null} if {@code flights} is {@code null} + */ + default List toFlightResponseList(List flights) { + + if (flights == null) { + return null; + } + + return flights.stream() + .map(flighttoFlightResponseMapper::map) + .collect(Collectors.toList()); + + } + + /** + * Initializes and returns an instance of the {@link CustomPageFlightToCustomPagingFlightResponseMapper}. + * + * @return an instance of the mapper + */ + static CustomPageFlightToCustomPagingFlightResponseMapper initialize() { + return Mappers.getMapper(CustomPageFlightToCustomPagingFlightResponseMapper.class); + } + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/FlightEntityToFlightMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/FlightEntityToFlightMapper.java new file mode 100644 index 000000000..2c6ce7121 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/FlightEntityToFlightMapper.java @@ -0,0 +1,26 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.entity.FlightEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting a {@link FlightEntity} to a {@link Flight}. + * This interface extends the {@link BaseMapper} interface, enabling automatic mapping + * between {@link FlightEntity} and {@link Flight} objects. + */ +@Mapper +public interface FlightEntityToFlightMapper extends BaseMapper { + + /** + * Initializes and returns an instance of the {@link FlightEntityToFlightMapper}. + * + * @return an instance of the mapper + */ + static FlightEntityToFlightMapper initialize() { + return Mappers.getMapper(FlightEntityToFlightMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/FlightToFlightResponseMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/FlightToFlightResponseMapper.java new file mode 100644 index 000000000..d818b5d35 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/FlightToFlightResponseMapper.java @@ -0,0 +1,28 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting a {@link Flight} to a {@link FlightResponse}. + * This interface extends the {@link BaseMapper} interface, enabling automatic mapping + * between {@link Flight} and {@link FlightResponse} objects. + */ +@Mapper +public interface FlightToFlightResponseMapper extends BaseMapper { + + /** + * Initializes and returns an instance of the {@link FlightToFlightResponseMapper}. + * This method is used to obtain a mapper instance for converting a {@link Flight} + * object into a {@link FlightResponse} object. + * + * @return an instance of the {@link FlightToFlightResponseMapper} mapper + */ + static FlightToFlightResponseMapper initialize() { + return Mappers.getMapper(FlightToFlightResponseMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/ListFlightEntityToListFlightMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/ListFlightEntityToListFlightMapper.java new file mode 100644 index 000000000..de4ffbfc4 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/ListFlightEntityToListFlightMapper.java @@ -0,0 +1,47 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.entity.FlightEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Mapper interface that converts a list of {@link FlightEntity} objects to a list of {@link Flight} objects. + * It leverages MapStruct for automatic mapping between entity and domain objects. + */ +@Mapper +public interface ListFlightEntityToListFlightMapper { + + FlightEntityToFlightMapper flightEntityToFlightMapper = Mappers.getMapper(FlightEntityToFlightMapper.class); + + /** + * Converts a list of {@link FlightEntity} objects to a list of {@link Flight} objects. + * + * @param flightsEntities the list of {@link FlightEntity} objects to be converted + * @return a list of {@link Flight} objects, or {@code null} if {@code flightsEntities} is {@code null} + */ + default List toFlightList(List flightsEntities) { + + if (flightsEntities == null) { + return null; + } + + return flightsEntities.stream() + .map(flightEntityToFlightMapper::map) + .collect(Collectors.toList()); + + } + + /** + * Initializes and returns an instance of the {@link ListFlightEntityToListFlightMapper}. + * + * @return an instance of the mapper + */ + static ListFlightEntityToListFlightMapper initialize() { + return Mappers.getMapper(ListFlightEntityToListFlightMapper.class); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/UpdateFlightRequestToFlightEntityMapper.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/UpdateFlightRequestToFlightEntityMapper.java new file mode 100644 index 000000000..519ec1a67 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/model/mapper/flight/UpdateFlightRequestToFlightEntityMapper.java @@ -0,0 +1,59 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.common.model.mapper.BaseMapper; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface for converting and updating a {@link FlightEntity} using data from an {@link UpdateFlightRequest}. + * This interface extends the {@link BaseMapper} interface, enabling automatic mapping + * between {@link UpdateFlightRequest} and {@link FlightEntity}. + */ +@Mapper +public interface UpdateFlightRequestToFlightEntityMapper extends BaseMapper { + + /** + * Updates the fields of an existing {@link FlightEntity} with data from an {@link UpdateFlightRequest}. + * This method maps the fields from the request to the entity, specifically + * updating flight details such as airports, times, and price. + * + * @param flightEntity the {@link FlightEntity} to be updated + * @param updateFlightRequest the {@link UpdateFlightRequest} containing the new data + */ + @Named("mapForUpdate") + default void updateFlightMapper(final FlightEntity flightEntity, final UpdateFlightRequest updateFlightRequest, + AirportEntity departureAirportEntity, AirportEntity arrivalAirportEntity) { + flightEntity.setFromAirport(AirportEntity.builder() + .id(updateFlightRequest.getFromAirportId()) + .name(departureAirportEntity.getName()) + .cityName(departureAirportEntity.getCityName()) + .build()); + + flightEntity.setToAirport(AirportEntity.builder() + .id(updateFlightRequest.getToAirportId()) + .name(arrivalAirportEntity.getName()) + .cityName(arrivalAirportEntity.getCityName()) + .build()); + + flightEntity.setDepartureTime(updateFlightRequest.getDepartureTime()); + flightEntity.setArrivalTime(updateFlightRequest.getArrivalTime()); + flightEntity.setPrice(updateFlightRequest.getPrice()); + } + + /** + * Initializes and returns an instance of the {@link UpdateFlightRequestToFlightEntityMapper}. + * This method is used to obtain a mapper instance for mapping an {@link UpdateFlightRequest} + * to a {@link FlightEntity}, including updating an existing entity. + * + * @return an instance of the {@link UpdateFlightRequestToFlightEntityMapper} mapper + */ + static UpdateFlightRequestToFlightEntityMapper initialize() { + return Mappers.getMapper(UpdateFlightRequestToFlightEntityMapper.class); + } + +} + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/AirportRepository.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/AirportRepository.java new file mode 100644 index 000000000..d19298446 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/AirportRepository.java @@ -0,0 +1,20 @@ +package com.example.demo.flight.repository; + +import com.example.demo.flight.model.entity.AirportEntity; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * Repository interface for managing {@link AirportEntity} entities in the MongoDB database. + * Provides CRUD operations and custom query methods for handling {@link AirportEntity} records. + */ +public interface AirportRepository extends MongoRepository { + + /** + * Checks if a task with the specified name already exists in the database. + * + * @param name the name of the task to check for existence. + * @return {@code true} if a task with the specified name exists, {@code false} otherwise. + */ + boolean existsByName(String name); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/FlightRepository.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/FlightRepository.java new file mode 100644 index 000000000..c827ddc14 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/FlightRepository.java @@ -0,0 +1,13 @@ +package com.example.demo.flight.repository; + + +import com.example.demo.flight.model.entity.FlightEntity; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * Repository interface for managing {@link FlightRepository} entities in the Couchbase database. + * Provides CRUD operations and custom query methods for handling {@link FlightEntity} records. + */ +public interface FlightRepository extends MongoRepository { + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/SearchFlightRepository.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/SearchFlightRepository.java new file mode 100644 index 000000000..b1fcaab16 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/repository/SearchFlightRepository.java @@ -0,0 +1,34 @@ +package com.example.demo.flight.repository; + +import com.example.demo.flight.model.entity.FlightEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import java.time.LocalDateTime; + +/** + * Repository interface for searching flights. + */ +public interface SearchFlightRepository extends MongoRepository { + + /** + * Finds flights based on the specified criteria and returns a paginated result. + * + * @param fromAirportId the ID of the departure airport. + * @param toAirportId the ID of the destination airport. + * @param start the start of the departure time range. + * @param end the end of the departure time range. + * @param pageable pagination details. + * @return a {@link Page} of {@link FlightEntity} matching the search criteria. + */ + @Query("{ 'fromAirport.id': ?0, 'toAirport.id': ?1, 'departureTime': { $gte: ?2, $lte: ?3 } }") + Page findFlights( + String fromAirportId, + String toAirportId, + LocalDateTime start, + LocalDateTime end, + Pageable pageable); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportCreateService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportCreateService.java new file mode 100644 index 000000000..a79462c65 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportCreateService.java @@ -0,0 +1,19 @@ +package com.example.demo.flight.service.airport; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; + +/** + * Service interface for creating an airport in the system. + */ +public interface AirportCreateService { + + /** + * Create a new airport to the database. + * + * @param createAirportRequest the request object containing the details of the CreateAirportRequest to be saved. + * @return the saved {@link Airport} domain object. + */ + Airport createAirport(CreateAirportRequest createAirportRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportDeleteService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportDeleteService.java new file mode 100644 index 000000000..8483d4436 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportDeleteService.java @@ -0,0 +1,15 @@ +package com.example.demo.flight.service.airport; + +/** + * Service interface for deleting an airport in the system. + */ +public interface AirportDeleteService { + + /** + * Deletes an airport by its ID. + * + * @param id the ID of the airport to be deleted. + */ + void deleteAirportById(String id); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportReadService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportReadService.java new file mode 100644 index 000000000..7f3a9fce9 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportReadService.java @@ -0,0 +1,28 @@ +package com.example.demo.flight.service.airport; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.model.Airport; + +/** + * Service interface for retrieving an airport in the system. + */ +public interface AirportReadService { + + /** + * Retrieves an airport by its ID. + * + * @param id the ID of the task to be retrieved. + * @return the {@link Airport} entity with the specified ID. + */ + Airport getAirportById(final String id); + + /** + * Retrieves all airports with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of airports, containing a list of airports and pagination details. + */ + CustomPage getAllAirports(final CustomPagingRequest customPagingRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportService.java new file mode 100644 index 000000000..e2febaf97 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportService.java @@ -0,0 +1,55 @@ +package com.example.demo.flight.service.airport; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; + +/** + * Service interface for managing an airport in the system. + */ +public interface AirportService { + + /** + * Create a new airport to the database. + * + * @param createAirportRequest the request object containing the details of the CreateAirportRequest to be saved. + * @return the saved {@link Airport} domain object. + */ + Airport createAirport(CreateAirportRequest createAirportRequest); + + /** + * Retrieves an airport by its ID. + * + * @param id the ID of the airport to be retrieved. + * @return the {@link Airport} entity with the specified ID. + */ + Airport getAirportById(final String id); + + /** + * Retrieves all airports with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of airports, containing a list of airports and pagination details. + */ + CustomPage getAllAirports(final CustomPagingRequest customPagingRequest); + + /** + * Updates an existing an airport by its ID. + * + * @param id the ID of the airport to be updated. + * @param updateAirportRequest the request object containing the updated details of the airport. + * @return the updated {@link Airport} entity. + */ + Airport updateAirportById(final String id, final UpdateAirportRequest updateAirportRequest); + + /** + * Deletes an airport by its ID. + * + * @param id the ID of the airport to be deleted. + */ + void deleteAirportById(String id); + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportUpdateService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportUpdateService.java new file mode 100644 index 000000000..cb9d0dde1 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/AirportUpdateService.java @@ -0,0 +1,20 @@ +package com.example.demo.flight.service.airport; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; + +/** + * Service interface for updating an airport in the system. + */ +public interface AirportUpdateService { + + /** + * Updates an existing an airport by its ID. + * + * @param id the ID of the airport to be updated. + * @param updateAirportRequest the request object containing the updated details of the airport. + * @return the updated {@link Airport} entity. + */ + Airport updateAirportById(final String id, final UpdateAirportRequest updateAirportRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportCreateServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportCreateServiceImpl.java new file mode 100644 index 000000000..c02a86909 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportCreateServiceImpl.java @@ -0,0 +1,48 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.mapper.airport.AirportEntityToAirportMapper; +import com.example.demo.flight.model.mapper.airport.CreateAirportRequestToAirportEntityMapper; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.service.airport.AirportCreateService; +import com.example.demo.flight.utils.AirportUtilityClass; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service implementation for creating an airport in the system. + */ +@Service +@RequiredArgsConstructor +public class AirportCreateServiceImpl implements AirportCreateService { + + private final AirportRepository airportRepository; + + private final CreateAirportRequestToAirportEntityMapper createAirportRequestToAirportEntityMapper = + CreateAirportRequestToAirportEntityMapper.initialize(); + + private final AirportEntityToAirportMapper airportEntityToAirportMapper = + AirportEntityToAirportMapper.initialize(); + + /** + * Create a new airport to the database. + * + * @param createAirportRequest the request object containing the details of the CreateAirportRequest to be saved. + * @return the saved {@link Airport} domain object. + */ + @Override + public Airport createAirport(CreateAirportRequest createAirportRequest) { + + AirportUtilityClass.checkAirportNameUniqueness(airportRepository, createAirportRequest.getName()); + + AirportEntity airportEntityToBeSaved = createAirportRequestToAirportEntityMapper.mapForSaving(createAirportRequest); + + AirportEntity savedAirportEntity = airportRepository.save(airportEntityToBeSaved); + + return airportEntityToAirportMapper.map(savedAirportEntity); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportDeleteServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportDeleteServiceImpl.java new file mode 100644 index 000000000..20a190108 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportDeleteServiceImpl.java @@ -0,0 +1,34 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.service.airport.AirportDeleteService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service implementation for deleting an airport in the system. + */ +@Service +@RequiredArgsConstructor +public class AirportDeleteServiceImpl implements AirportDeleteService { + + private final AirportRepository airportRepository; + + /** + * Deletes an airport by its ID. + * + * @param id the ID of the airport to be deleted. + */ + @Override + public void deleteAirportById(String id) { + + AirportEntity airportEntityToBeDeleted = airportRepository.findById(id) + .orElseThrow(()->new AirportNotFoundException("With given id = " + id)); + + airportRepository.delete(airportEntityToBeDeleted); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportReadServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportReadServiceImpl.java new file mode 100644 index 000000000..983150adc --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportReadServiceImpl.java @@ -0,0 +1,71 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.mapper.airport.AirportEntityToAirportMapper; +import com.example.demo.flight.model.mapper.airport.ListAirportEntityToListAirportMapper; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.service.airport.AirportReadService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * Service implementation for retrieving an airport in the system. + */ +@Service +@RequiredArgsConstructor +public class AirportReadServiceImpl implements AirportReadService { + + private final AirportRepository airportRepository; + + private final AirportEntityToAirportMapper airportEntityToAirportMapper = + AirportEntityToAirportMapper.initialize(); + + private final ListAirportEntityToListAirportMapper listAirportEntityToListAirportMapper = + ListAirportEntityToListAirportMapper.initialize(); + + /** + * Retrieves an airport by its ID. + * + * @param id the ID of the task to be retrieved. + * @return the {@link Airport} entity with the specified ID. + */ + @Override + public Airport getAirportById(String id) { + + AirportEntity airportEntityFromDb = airportRepository.findById(id) + .orElseThrow(()->new AirportNotFoundException("Airport given id cant found")); + + return airportEntityToAirportMapper.map(airportEntityFromDb); + + } + + /** + * Retrieves all airports with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of tasks, containing a list of airports and pagination details. + */ + @Override + public CustomPage getAllAirports(CustomPagingRequest customPagingRequest) { + + Page taskEntitiesListPage = airportRepository.findAll(customPagingRequest.toPageable()); + + if (taskEntitiesListPage.getContent().isEmpty()) { + throw new AirportNotFoundException("Couldn't find any airport"); + } + + final List productDomainModels = listAirportEntityToListAirportMapper + .toAirportList(taskEntitiesListPage.getContent()); + + return CustomPage.of(productDomainModels, taskEntitiesListPage); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportServiceImpl.java new file mode 100644 index 000000000..52c80fa2d --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportServiceImpl.java @@ -0,0 +1,80 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.service.airport.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service implementation for managing an airport in the system. + */ +@Service +@RequiredArgsConstructor +public class AirportServiceImpl implements AirportService { + + private final AirportCreateService airportCreateService; + private final AirportReadService airportReadService; + private final AirportUpdateService airportUpdateService; + private final AirportDeleteService airportDeleteService; + + /** + * Create a new airport to the database. + * + * @param createAirportRequest the request object containing the details of the CreateAirportRequest to be saved. + * @return the saved {@link Airport} domain object. + */ + @Override + public Airport createAirport(CreateAirportRequest createAirportRequest) { + return airportCreateService.createAirport(createAirportRequest); + } + + /** + * Retrieves an airport by its ID. + * + * @param id the ID of the airport to be retrieved. + * @return the {@link Airport} entity with the specified ID. + */ + @Override + public Airport getAirportById(String id) { + return airportReadService.getAirportById(id); + } + + /** + * Retrieves all airports with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of airports, containing a list of airports and pagination details. + */ + @Override + public CustomPage getAllAirports(CustomPagingRequest customPagingRequest) { + return airportReadService.getAllAirports(customPagingRequest); + } + + /** + * Updates an existing an airport by its ID. + * + * @param id the ID of the airport to be updated. + * @param updateAirportRequest the request object containing the updated details of the airport. + * @return the updated {@link Airport} entity. + */ + @Override + public Airport updateAirportById(String id, UpdateAirportRequest updateAirportRequest) { + return airportUpdateService.updateAirportById(id,updateAirportRequest); + } + + /** + * Deletes an airport by its ID. + * + * @param id the ID of the airport to be deleted. + */ + @Override + public void deleteAirportById(String id) { + airportDeleteService.deleteAirportById(id); + } + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportUpdateServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportUpdateServiceImpl.java new file mode 100644 index 000000000..a3ce6406b --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/airport/impl/AirportUpdateServiceImpl.java @@ -0,0 +1,53 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.mapper.airport.AirportEntityToAirportMapper; +import com.example.demo.flight.model.mapper.airport.UpdateAirportRequestToAirportEntityMapper; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.service.airport.AirportUpdateService; +import com.example.demo.flight.utils.AirportUtilityClass; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service implementation for updating an airport in the system. + */ +@Service +@RequiredArgsConstructor +public class AirportUpdateServiceImpl implements AirportUpdateService { + + private final AirportRepository airportRepository; + + private final UpdateAirportRequestToAirportEntityMapper updateAirportRequestToAirportEntityMapper = + UpdateAirportRequestToAirportEntityMapper.initialize(); + + private final AirportEntityToAirportMapper airportEntityToAirportMapper = + AirportEntityToAirportMapper.initialize(); + + /** + * Updates an existing an airport by its ID. + * + * @param id the ID of the airport to be updated. + * @param updateAirportRequest the request object containing the updated details of the airport. + * @return the updated {@link Airport} entity. + */ + @Override + public Airport updateAirportById(String id, UpdateAirportRequest updateAirportRequest) { + + AirportUtilityClass.checkAirportNameUniqueness(airportRepository, updateAirportRequest.getName()); + + AirportEntity airportEntity = airportRepository.findById(id) + .orElseThrow(()->new AirportNotFoundException("Airport given id cant found" + id)); + + updateAirportRequestToAirportEntityMapper.updateAirportMapper(airportEntity,updateAirportRequest); + + AirportEntity updatedAirportEntity = airportRepository.save(airportEntity); + + return airportEntityToAirportMapper.map(updatedAirportEntity); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightCreateService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightCreateService.java new file mode 100644 index 000000000..53f2281c5 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightCreateService.java @@ -0,0 +1,19 @@ +package com.example.demo.flight.service.flight; + +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; + +/** + * Service interface for creating a flight in the system. + */ +public interface FlightCreateService { + + /** + * Creates a new flight in the system. + * + * @param createFlightRequest the request object containing the details of the flight to be created. + * @return the created {@link Flight} entity. + */ + Flight createFlight(CreateFlightRequest createFlightRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightDeleteService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightDeleteService.java new file mode 100644 index 000000000..98d34b372 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightDeleteService.java @@ -0,0 +1,15 @@ +package com.example.demo.flight.service.flight; + +/** + * Service interface for deleting a flight in the system. + */ +public interface FlightDeleteService { + + /** + * Deletes a flight by its ID. + * + * @param id the ID of the flight to be deleted. + */ + void deleteFlightById(String id); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightReadService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightReadService.java new file mode 100644 index 000000000..904bcfcaa --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightReadService.java @@ -0,0 +1,28 @@ +package com.example.demo.flight.service.flight; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.model.Flight; + +/** + * Service implementation for retrieving a flight in the system. + */ +public interface FlightReadService { + + /** + * Retrieves a flight by its ID. + * + * @param id the ID of the flight to be retrieved. + * @return the {@link Flight} entity with the specified ID. + */ + Flight getFlightById(final String id); + + /** + * Retrieves all flights with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of flights, containing a list of flights and pagination details. + */ + CustomPage getAllFlights(final CustomPagingRequest customPagingRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightService.java new file mode 100644 index 000000000..f002a6683 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightService.java @@ -0,0 +1,54 @@ +package com.example.demo.flight.service.flight; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; + +/** + * Service interface for managing a flight in the system. + */ +public interface FlightService { + + /** + * Creates a new flight in the system. + * + * @param createFlightRequest the request object containing the details of the flight to be created. + * @return the created {@link Flight} entity. + */ + Flight createFlight(CreateFlightRequest createFlightRequest); + + /** + * Retrieves a flight by its ID. + * + * @param id the ID of the flight to be retrieved. + * @return the {@link Flight} entity with the specified ID. + */ + Flight getFlightById(final String id); + + /** + * Retrieves all flights with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of flights, containing a list of flights and pagination details. + */ + CustomPage getAllFlights(final CustomPagingRequest customPagingRequest); + + /** + * Updates an existing a flight by its ID. + * + * @param id the ID of the flight to be updated. + * @param updateFlightRequest the request object containing the updated details of the flight. + * @return the updated {@link Flight} entity. + */ + Flight updateFlightById(final String id, final UpdateFlightRequest updateFlightRequest); + + /** + * Deletes a flight by its ID. + * + * @param id the ID of the flight to be deleted. + */ + void deleteFlightById(String id); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightUpdateService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightUpdateService.java new file mode 100644 index 000000000..3f23db096 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/FlightUpdateService.java @@ -0,0 +1,20 @@ +package com.example.demo.flight.service.flight; + +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; + +/** + * Service interface for updating a flight in the system. + */ +public interface FlightUpdateService { + + /** + * Updates an existing a flight by its ID. + * + * @param id the ID of the flight to be updated. + * @param updateFlightRequest the request object containing the updated details of the flight. + * @return the updated {@link Flight} entity. + */ + Flight updateFlightById(final String id, final UpdateFlightRequest updateFlightRequest); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/SearchFlightService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/SearchFlightService.java new file mode 100644 index 000000000..633693330 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/SearchFlightService.java @@ -0,0 +1,20 @@ +package com.example.demo.flight.service.flight; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.SearchFlightRequest; + +/** + * Service interface for search operations. + */ +public interface SearchFlightService { + + /** + * Searches for flights based on the given criteria and returns a paginated result. + * + * @param request the search criteria including airport IDs, departure, and optional return times. + * @return a {@link CustomPage} of {@link Flight} with matching flights and pagination details. + */ + CustomPage searchFlights(SearchFlightRequest request); + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightCreateServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightCreateServiceImpl.java new file mode 100644 index 000000000..91677f960 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightCreateServiceImpl.java @@ -0,0 +1,54 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.CreateFlightRequestToFlightEntityMapper; +import com.example.demo.flight.model.mapper.flight.FlightEntityToFlightMapper; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.repository.FlightRepository; +import com.example.demo.flight.service.flight.FlightCreateService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + + +/** + * Service interface for creating a flight in the system. + */ +@Service +@RequiredArgsConstructor +public class FlightCreateServiceImpl implements FlightCreateService { + + private final FlightRepository flightRepository; + private final AirportRepository airportRepository; + + private final CreateFlightRequestToFlightEntityMapper createFlightRequestToFlightEntityMapper = + CreateFlightRequestToFlightEntityMapper.initialize(); + + private final FlightEntityToFlightMapper flightEntityToFlightMapper = + FlightEntityToFlightMapper.initialize(); + + /** + * Creates a new flight in the system. + * + * @param createFlightRequest the request object containing the details of the flight to be created. + * @return the created {@link Flight} entity. + */ + @Override + public Flight createFlight(CreateFlightRequest createFlightRequest) { + + AirportEntity departureAirport = airportRepository.findById(createFlightRequest.getFromAirportId()) + .orElseThrow(() -> new AirportNotFoundException("Departure airport not found with id " + createFlightRequest.getFromAirportId())); + AirportEntity arrivalAirport = airportRepository.findById(createFlightRequest.getToAirportId()) + .orElseThrow(() -> new AirportNotFoundException("Arrival airport not found with id " + createFlightRequest.getToAirportId())); + + FlightEntity flightEntityTobeSaved = createFlightRequestToFlightEntityMapper.mapForSaving(createFlightRequest, departureAirport, arrivalAirport); + + FlightEntity savedFlight = flightRepository.save(flightEntityTobeSaved); + + return flightEntityToFlightMapper.map(savedFlight); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightDeleteServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightDeleteServiceImpl.java new file mode 100644 index 000000000..c08a2a4a8 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightDeleteServiceImpl.java @@ -0,0 +1,34 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.repository.FlightRepository; +import com.example.demo.flight.service.flight.FlightDeleteService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service implementation for deleting a flight in the system. + */ +@Service +@RequiredArgsConstructor +public class FlightDeleteServiceImpl implements FlightDeleteService { + + private final FlightRepository flightRepository; + + /** + * Deletes a flight by its ID. + * + * @param id the ID of the flight to be deleted. + */ + @Override + public void deleteFlightById(String id) { + + FlightEntity flightEntityToBeDeleted = flightRepository.findById(id) + .orElseThrow(()->new FlightNotFoundException("With given id = " + id)); + + flightRepository.delete(flightEntityToBeDeleted); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightReadServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightReadServiceImpl.java new file mode 100644 index 000000000..6747ec0c1 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightReadServiceImpl.java @@ -0,0 +1,72 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.FlightEntityToFlightMapper; +import com.example.demo.flight.model.mapper.flight.ListFlightEntityToListFlightMapper; +import com.example.demo.flight.repository.FlightRepository; +import com.example.demo.flight.service.flight.FlightReadService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * Service implementation for retrieving a flight in the system. + */ +@Service +@RequiredArgsConstructor +public class FlightReadServiceImpl implements FlightReadService { + + private final FlightRepository flightRepository; + + private final FlightEntityToFlightMapper flightEntityToFlightMapper = + FlightEntityToFlightMapper.initialize(); + + private final ListFlightEntityToListFlightMapper listFlightEntityToListFlightMapper = + ListFlightEntityToListFlightMapper.initialize(); + + /** + * Retrieves a flight by its ID. + * + * @param id the ID of the flight to be retrieved. + * @return the {@link Flight} entity with the specified ID. + */ + @Override + public Flight getFlightById(String id) { + + FlightEntity flightEntityFromDb = flightRepository.findById(id) + .orElseThrow(()->new FlightNotFoundException("Flight given id " + id + " +can't found ")); + + return flightEntityToFlightMapper.map(flightEntityFromDb); + + } + + /** + * Retrieves all flights with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of flights, containing a list of flights and pagination details. + */ + @Override + public CustomPage getAllFlights(CustomPagingRequest customPagingRequest) { + + Page flightEntitiesListPage = flightRepository.findAll(customPagingRequest.toPageable()); + + if (flightEntitiesListPage.getContent().isEmpty()) { + throw new FlightNotFoundException("Couldn't find any airport"); + } + + final List fligtDomainList = listFlightEntityToListFlightMapper + .toFlightList(flightEntitiesListPage.getContent()); + + return CustomPage.of(fligtDomainList, flightEntitiesListPage); + + } + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightServiceImpl.java new file mode 100644 index 000000000..0c30930dd --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightServiceImpl.java @@ -0,0 +1,79 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.service.flight.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service interface for managing a flight in the system. + */ +@Service +@RequiredArgsConstructor +public class FlightServiceImpl implements FlightService { + + private final FlightCreateService flightCreateService; + private final FlightReadService flightReadService; + private final FlightUpdateService flightUpdateService; + private final FlightDeleteService flightDeleteService; + + /** + * Creates a new flight in the system. + * + * @param createFlightRequest the request object containing the details of the flight to be created. + * @return the created {@link Flight} entity. + */ + @Override + public Flight createFlight(CreateFlightRequest createFlightRequest) { + return flightCreateService.createFlight(createFlightRequest); + } + + /** + * Retrieves a flight by its ID. + * + * @param id the ID of the flight to be retrieved. + * @return the {@link Flight} entity with the specified ID. + */ + @Override + public Flight getFlightById(String id) { + return flightReadService.getFlightById(id); + } + + /** + * Retrieves all flights with pagination support. + * + * @param customPagingRequest the request object containing paging parameters (e.g., page size, page number). + * @return a {@link CustomPage} of flights, containing a list of flights and pagination details. + */ + @Override + public CustomPage getAllFlights(CustomPagingRequest customPagingRequest) { + return flightReadService.getAllFlights(customPagingRequest); + } + + /** + * Updates an existing a flight by its ID. + * + * @param id the ID of the flight to be updated. + * @param updateFlightRequest the request object containing the updated details of the flight. + * @return the updated {@link Flight} entity. + */ + @Override + public Flight updateFlightById(String id, UpdateFlightRequest updateFlightRequest) { + return flightUpdateService.updateFlightById(id,updateFlightRequest); + } + + /** + * Deletes a flight by its ID. + * + * @param id the ID of the flight to be deleted. + */ + @Override + public void deleteFlightById(String id) { + flightDeleteService.deleteFlightById(id); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightUpdateServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightUpdateServiceImpl.java new file mode 100644 index 000000000..4fbc33358 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/FlightUpdateServiceImpl.java @@ -0,0 +1,61 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.FlightEntityToFlightMapper; +import com.example.demo.flight.model.mapper.flight.UpdateFlightRequestToFlightEntityMapper; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.repository.FlightRepository; +import com.example.demo.flight.service.flight.FlightUpdateService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * Service implementation for updating a flight in the system. + */ +@Service +@RequiredArgsConstructor +public class FlightUpdateServiceImpl implements FlightUpdateService { + + private final FlightRepository flightRepository; + private final AirportRepository airportRepository; + + private final UpdateFlightRequestToFlightEntityMapper updateFlightRequestToFlightEntityMapper = + UpdateFlightRequestToFlightEntityMapper.initialize(); + + private final FlightEntityToFlightMapper flightEntityToFlightMapper = + FlightEntityToFlightMapper.initialize(); + + /** + * Updates an existing a flight by its ID. + * + * @param id the ID of the flight to be updated. + * @param updateFlightRequest the request object containing the updated details of the flight. + * @return the updated {@link Flight} entity. + */ + @Override + public Flight updateFlightById(String id, UpdateFlightRequest updateFlightRequest) { + + FlightEntity flightEntity = flightRepository.findById(id) + .orElseThrow(()->new FlightNotFoundException("Flight given id cant found " + id)); + + AirportEntity departureAirportEntity = airportRepository.findById(updateFlightRequest.getFromAirportId()) + .orElseThrow(() -> new AirportNotFoundException("Departure airport not found with id " + updateFlightRequest.getFromAirportId())); + + AirportEntity arrivalAirportEntity =airportRepository.findById(updateFlightRequest.getToAirportId()) + .orElseThrow(() -> new AirportNotFoundException("Arrival airport not found with id " + updateFlightRequest.getToAirportId())); + + updateFlightRequestToFlightEntityMapper.updateFlightMapper(flightEntity,updateFlightRequest, + departureAirportEntity,arrivalAirportEntity); + + FlightEntity updatedFlightEntity = flightRepository.save(flightEntity); + + return flightEntityToFlightMapper.map(updatedFlightEntity); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/SearchFlightServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/SearchFlightServiceImpl.java new file mode 100644 index 000000000..212c64031 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/service/flight/impl/SearchFlightServiceImpl.java @@ -0,0 +1,95 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.SearchFlightRequest; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.ListFlightEntityToListFlightMapper; +import com.example.demo.flight.repository.SearchFlightRepository; +import com.example.demo.flight.service.flight.SearchFlightService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * Service implementation for searching flights. + */ +@Service +@RequiredArgsConstructor +public class SearchFlightServiceImpl implements SearchFlightService { + + private final SearchFlightRepository searchFlightRepository; + + private final ListFlightEntityToListFlightMapper listFlightEntityToListFlightMapper = + ListFlightEntityToListFlightMapper.initialize(); + + /** + * Searches for flights based on the given criteria and returns a paginated result. + * + * @param request the search criteria including airport IDs, departure, and optional return times. + * @return a {@link CustomPage} of {@link Flight} with matching flights and pagination details. + */ + @Override + public CustomPage searchFlights(SearchFlightRequest request) { + + // Convert the given departure time to LocalDateTime representing the start of the day (midnight). + // departureStart is used to ensure the search includes flights departing at any time during the given date. + // Example: If request.getDepartureTime() is "2025-01-20T15:30:00", departureStart will be "2025-01-20T00:00:00". + LocalDateTime departureStart = request.getDepartureTime().toLocalDate().atStartOfDay(); + + // Calculate the end of the departure day (just before midnight of the next day). + // departureEnd ensures that the search captures flights up until the last moment of the specified departure date. + // Example: If departureStart is "2025-01-20T00:00:00", departureEnd will be "2025-01-20T23:59:59.999999999". + LocalDateTime departureEnd = departureStart.plusDays(1).minusNanos(1); + + Pageable pageable = request.toPageable(); + + // Fetch one-way flights + Page departureFlightEntities = searchFlightRepository.findFlights( + request.getFromAirportId(), + request.getToAirportId(), + departureStart, + departureEnd, + pageable); + + List flightList = listFlightEntityToListFlightMapper.toFlightList(departureFlightEntities.getContent()); + + // Handle round-trip flights if arrivalTime is provided + Optional.ofNullable(request.getArrivalTime()).ifPresent(arrivalTime -> { + + // Convert the given arrival time to LocalDateTime representing the start of the day (midnight). + // returnStart ensures the search includes return flights arriving at any time during the given arrival date. + // Example: If arrivalTime is "2025-01-25T10:45:00", returnStart will be "2025-01-25T00:00:00". + LocalDateTime returnStart = arrivalTime.toLocalDate().atStartOfDay(); + + // Calculate the end of the return day (just before midnight of the next day). + // returnEnd ensures that the search captures return flights up until the last moment of the specified arrival date. + // Example: If returnStart is "2025-01-25T00:00:00", returnEnd will be "2025-01-25T23:59:59.999999999". + LocalDateTime returnEnd = returnStart.plusDays(1).minusNanos(1); + + // Fetch return flights + Page returnFlightEntities = searchFlightRepository.findFlights( + request.getToAirportId(), + request.getFromAirportId(), + returnStart, + returnEnd, + pageable); + + List returnFlightList = + listFlightEntityToListFlightMapper.toFlightList(returnFlightEntities.getContent()); + + // Add return flights to the flight list + flightList.addAll(returnFlightList); + + }); + + return CustomPage.of(flightList, departureFlightEntities); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/AirportUtilityClass.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/AirportUtilityClass.java new file mode 100644 index 000000000..52743d0d4 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/AirportUtilityClass.java @@ -0,0 +1,23 @@ +package com.example.demo.flight.utils; + +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.repository.AirportRepository; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class AirportUtilityClass { + + /** + * Checks the uniqueness of an airport name in the repository. + * If an airport with the given name already exists, a {@link AirportNameAlreadyExistException} is thrown. + * + * @param airportName the name of the airport to be checked for uniqueness. + * @throws AirportNameAlreadyExistException if a task with the given name already exists in the repository. + */ + public void checkAirportNameUniqueness(final AirportRepository airportRepository, final String airportName) { + if (airportRepository.existsByName(airportName)) { + throw new AirportNameAlreadyExistException("With given airport name = " + airportName); + } + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/CustomLocalDateTimeDeserializer.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/CustomLocalDateTimeDeserializer.java new file mode 100644 index 000000000..767d3b7d2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/CustomLocalDateTimeDeserializer.java @@ -0,0 +1,23 @@ +package com.example.demo.flight.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class CustomLocalDateTimeDeserializer extends JsonDeserializer { + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String date = p.getText(); + if (date != null && !date.isEmpty()) { + // Parse the date part only and return a LocalDateTime at midnight (start of the day) + return LocalDateTime.parse(date + "T00:00:00", DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + return null; // Return null if the value is empty or null + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/annotations/ValidArrivalTime.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/annotations/ValidArrivalTime.java new file mode 100644 index 000000000..1c884ce70 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/annotations/ValidArrivalTime.java @@ -0,0 +1,25 @@ +package com.example.demo.flight.utils.annotations; + +import com.example.demo.flight.utils.validator.ArrivalTimeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Custom annotation for validating arrival time. + */ +@Target({ ElementType.TYPE, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ArrivalTimeValidator.class) +public @interface ValidArrivalTime { + String message() default "Arrival time must be the same as or later than departure time!"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/validator/ArrivalTimeValidator.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/validator/ArrivalTimeValidator.java new file mode 100644 index 000000000..1dc2bfd89 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/flight/utils/validator/ArrivalTimeValidator.java @@ -0,0 +1,63 @@ +package com.example.demo.flight.utils.validator; + +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.dto.request.flight.SearchFlightRequest; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.utils.annotations.ValidArrivalTime; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * Validates that the arrival time is not earlier than the departure time in {@link Object}. + */ +@Component +public class ArrivalTimeValidator implements ConstraintValidator { + + /** + * Validates that the arrival time is not earlier than the departure time. + * This method works for both CreateFlightRequest and UpdateFlightRequest. + * + * @param value The object being validated. It can either be a CreateFlightRequest or UpdateFlightRequest. + * @param context The validation context. + * @return {@code true} if the arrival time is not earlier than the departure time, {@code false} otherwise. + */ + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (value == null) { + return true; // Return true if the value is null (skip validation) + } + + // Validate for CreateFlightRequest type + if (value instanceof CreateFlightRequest createFlightRequest) { + return isValidArrivalTime(createFlightRequest.getArrivalTime(), createFlightRequest.getDepartureTime()); + } + // Validate for UpdateFlightRequest type + else if (value instanceof UpdateFlightRequest updateFlightRequest) { + return isValidArrivalTime(updateFlightRequest.getArrivalTime(), updateFlightRequest.getDepartureTime()); + } + // Validate for SearchFlightRequest type + else if (value instanceof SearchFlightRequest searchFlightRequest) { + return isValidArrivalTime(searchFlightRequest.getArrivalTime(), searchFlightRequest.getDepartureTime()); + } + + return true; // Return true if the object type is not recognized + } + + /** + * Checks if the arrival time is after the departure time. + * + * @param arrivalTime The arrival time to validate. + * @param departureTime The departure time to compare against. + * @return {@code true} if arrival time is after departure time, {@code false} otherwise. + */ + private boolean isValidArrivalTime(LocalDateTime arrivalTime, LocalDateTime departureTime) { + if (arrivalTime != null && departureTime != null) { + return !arrivalTime.isBefore(departureTime); // Check if arrival time is not before departure time + } + return true; // If either time is null, the validation passes + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/aop/LoggerAspectJ.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/aop/LoggerAspectJ.java new file mode 100644 index 000000000..016cd318a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/aop/LoggerAspectJ.java @@ -0,0 +1,177 @@ +package com.example.demo.logging.aop; + +import com.example.demo.auth.exception.*; +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.logging.entity.LogEntity; +import com.example.demo.logging.service.LogService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.io.IOException; +import java.util.Optional; + +/** + * Aspect for logging method execution and exceptions in REST controllers. + * This class intercepts all method calls and exception throws in any class annotated with {@link org.springframework.web.bind.annotation.RestController}. + * It logs information about the HTTP request, response, and any exceptions that are thrown. + * The logs are saved to the database using the {@link LogService}. + */ +@Aspect +@Slf4j +@Component +@RequiredArgsConstructor +public class LoggerAspectJ { + + private final LogService logService; + + /** + * Pointcut that matches methods within classes annotated with {@link org.springframework.web.bind.annotation.RestController}. + */ + @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)") + public void restControllerPointcut() { + + } + + /** + * After throwing advice that logs exception details when an exception is thrown from a REST controller method. + * This method logs details such as the HTTP request URL, HTTP method, exception message, and the operation that threw the exception. + * It also retrieves the username from the {@link SecurityContextHolder} if available and logs it. + * + * @param joinPoint The join point representing the method execution. + * @param ex The exception thrown by the method. + */ + @AfterThrowing(pointcut = "restControllerPointcut()", throwing = "ex") + public void logAfterThrowing(JoinPoint joinPoint, Exception ex) { + + Optional requestAttributes = Optional.ofNullable( + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() + ); + + if (requestAttributes.isPresent()) { + + final HttpServletRequest request = requestAttributes.get().getRequest(); + + LogEntity logEntity = LogEntity.builder() + .endpoint(request.getRequestURL().toString()) + .method(request.getMethod()) + .message(ex.getMessage()) + .errorType(ex.getClass().getName()) + .status(getHttpStatusFromException(ex)) + .operation(joinPoint.getSignature().getName()) + .response(ex.getMessage()) + .build(); + + // Get the username from SecurityContextHolder and set it in logEntity + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken)) { + String username = authentication.getName(); + logEntity.setUserInfo(username); + } + + try { + logService.saveLogToDatabase(logEntity); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } else { + log.error("Request Attributes are null!"); + } + + } + + /** + * After returning advice that logs response details when a REST controller method successfully returns a result. + * This method logs information such as the HTTP request URL, HTTP method, operation, and response message. + * It also includes the HTTP status code in the log. + * + * @param joinPoint The join point representing the method execution. + * @param result The result returned by the method. + * @throws IOException If there is an issue serializing the response object. + */ + @AfterReturning(value = "restControllerPointcut()", returning = "result") + public void logAfterReturning(JoinPoint joinPoint, Object result) throws IOException { + + Optional requestAttributes = Optional.ofNullable( + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() + ); + + if (requestAttributes.isPresent()) { + + final HttpServletRequest request = requestAttributes.get().getRequest(); + final HttpServletResponse response = requestAttributes.get().getResponse(); + + String responseObject = ""; + + LogEntity logEntity = LogEntity.builder() + .endpoint(request.getRequestURL().toString()) + .method(request.getMethod()) + .operation(joinPoint.getSignature().getName()) + .build(); + + if (result instanceof JsonNode) { + ObjectMapper objectMapper = new ObjectMapper(); + responseObject = objectMapper.writeValueAsString(result); + } else { + responseObject = result.toString(); + } + + logEntity.setResponse(responseObject); + logEntity.setMessage(responseObject); + Optional.ofNullable(response).ifPresent( + httpServletResponse -> logEntity.setStatus( + String.valueOf(HttpStatus.valueOf(httpServletResponse.getStatus() + )) + )); + + try { + logService.saveLogToDatabase(logEntity); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + } else { + log.error("Request Attributes are null!"); + } + } + + /** + * Retrieves the HTTP status from the exception type. + * This method maps specific exception classes to their corresponding HTTP status values. + * + * @param ex The exception thrown. + * @return The HTTP status as a string corresponding to the exception type. + */ + private String getHttpStatusFromException(Exception ex) { + return switch (ex.getClass().getSimpleName()) { + case "PasswordNotValidException" -> PasswordNotValidException.STATUS.name(); + case "RoleNotFoundException" -> RoleNotFoundException.STATUS.name(); + case "TokenAlreadyInvalidatedException" -> TokenAlreadyInvalidatedException.STATUS.name(); + case "UserAlreadyExistException" -> UserAlreadyExistException.STATUS.name(); + case "UserNotFoundException" -> UserNotFoundException.STATUS.name(); + case "UserStatusNotValidException" -> UserStatusNotValidException.STATUS.name(); + case "AirportNotFoundException" -> AirportNotFoundException.STATUS.name(); + case "AirportNameAlreadyExistException" -> AirportNameAlreadyExistException.STATUS.name(); + case "FlightNotFoundException" -> FlightNotFoundException.STATUS.name(); + default -> HttpStatus.INTERNAL_SERVER_ERROR.name(); + }; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/entity/LogEntity.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/entity/LogEntity.java new file mode 100644 index 000000000..80c610000 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/entity/LogEntity.java @@ -0,0 +1,57 @@ +package com.example.demo.logging.entity; + +import com.example.demo.common.model.entity.BaseEntity; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.time.LocalDateTime; + +/** + * Represents a log entity stored in the MongoDB {@code log-collection}. + * Captures details such as {@code message}, {@code endpoint}, {@code status}, and {@code time}. + * Extends {@link BaseEntity} for common timestamp fields. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@Document(collection = "log-collection") +public class LogEntity extends BaseEntity { + + @Id + @Indexed(unique = true) + private String id; + + @Field + private String message; + + @Field + private String endpoint; + + @Field + private String method; + + @Field + private String status; + + @Field + private String userInfo; + + @Field + private String errorType; + + @Field + private String response; + + @Field + private String operation; + + @Field + private LocalDateTime time; +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/repository/LogRepository.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/repository/LogRepository.java new file mode 100644 index 000000000..9a387aa0f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/repository/LogRepository.java @@ -0,0 +1,15 @@ +package com.example.demo.logging.repository; + +import com.example.demo.logging.entity.LogEntity; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * Repository interface for {@link LogEntity}. + * This interface extends {@link MongoRepository} and provides methods for interacting with + * the Couchbase database for storing and retrieving {@link LogEntity} instances. + * The repository will automatically inherit basic CRUD operations such as save, find, and delete + * from {@link MongoRepository}. + */ +public interface LogRepository extends MongoRepository { + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/service/LogService.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/service/LogService.java new file mode 100644 index 000000000..dd278ae81 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/service/LogService.java @@ -0,0 +1,19 @@ +package com.example.demo.logging.service; + +import com.example.demo.logging.entity.LogEntity; + +/** + * Service interface for handling log-related operations. + * This interface defines a method for saving log entries to the database. + * It abstracts the logic for logging and allows interaction with a persistence layer to store log data. + */ +public interface LogService { + + /** + * Saves the provided {@link LogEntity} to the database. + * + * @param logEntity the {@link LogEntity} to be saved + */ + void saveLogToDatabase(final LogEntity logEntity); + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/service/impl/LogServiceImpl.java b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/service/impl/LogServiceImpl.java new file mode 100644 index 000000000..146567174 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/java/com/example/demo/logging/service/impl/LogServiceImpl.java @@ -0,0 +1,33 @@ +package com.example.demo.logging.service.impl; + +import com.example.demo.logging.entity.LogEntity; +import com.example.demo.logging.repository.LogRepository; +import com.example.demo.logging.service.LogService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * Service implementation for handling log-related operations. + * This interface defines a method for saving log entries to the database. + * It abstracts the logic for logging and allows interaction with a persistence layer to store log data. + */ +@Service +@RequiredArgsConstructor +public class LogServiceImpl implements LogService { + + private final LogRepository logRepository; + + /** + * Saves the provided {@link LogEntity} to the database. + * + * @param logEntity the {@link LogEntity} to be saved + */ + @Override + public void saveLogToDatabase(final LogEntity logEntity) { + logEntity.setTime(LocalDateTime.now()); + logRepository.save(logEntity); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/main/resources/application.yml b/jdk_21_maven/cs/rest/flight-search-api/src/main/resources/application.yml new file mode 100644 index 000000000..6c9479a44 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 1133 + +spring: + config: + import: optional:file:.env[.properties] + data: + mongodb: + host: ${MONGO_DB_HOST:localhost} + port: ${MONGO_DB_PORT:27017} + database: ${MONGO_DB_NAME:flightdatabase} + application: + name: flightsearchapi + +# SWAGGER +springdoc: + api-docs: + enabled: true + show-actuator: true + +# Prometheus +management: + endpoints: + web: + exposure: + include: + - "*" + - prometheus + prometheus: + metrics: + export: + enabled: true diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/controller/AuthControllerTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/controller/AuthControllerTest.java new file mode 100644 index 000000000..a79d0a746 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/controller/AuthControllerTest.java @@ -0,0 +1,190 @@ +package com.example.demo.auth.controller; + +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.User; +import com.example.demo.auth.model.dto.request.LoginRequest; +import com.example.demo.auth.model.dto.request.RegisterRequest; +import com.example.demo.auth.model.dto.request.TokenInvalidateRequest; +import com.example.demo.auth.model.dto.request.TokenRefreshRequest; +import com.example.demo.auth.model.dto.response.TokenResponse; +import com.example.demo.auth.model.enums.UserType; +import com.example.demo.auth.model.mapper.TokenToTokenResponseMapper; +import com.example.demo.auth.service.LoginService; +import com.example.demo.auth.service.LogoutService; +import com.example.demo.auth.service.RefreshTokenService; +import com.example.demo.auth.service.RegisterService; +import com.example.demo.base.AbstractRestControllerTest; +import com.example.demo.common.model.dto.response.CustomResponse; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Integration test class for {@link AuthController}. + * This test class mocks the dependencies of the controller and provides integration + * tests for authentication-related operations such as registration, login, refresh token, + * and logout. + */ +class AuthControllerTest extends AbstractRestControllerTest { + + @MockitoBean + RegisterService registerService; + + @MockitoBean + LoginService loginService; + + @MockitoBean + RefreshTokenService refreshTokenService; + + @MockitoBean + LogoutService logoutService; + + private final TokenToTokenResponseMapper tokenToTokenResponseMapper = TokenToTokenResponseMapper.initialize(); + + + @Test + void givenValidAdminRegisterRequestWithAdminCreate_whenRegisterAdmin_thenSuccess() throws Exception { + + // Given + final RegisterRequest request = RegisterRequest.builder() + .email("admincreate@example.com") + .password("password123") + .firstName("admin firstName") + .lastName("admin lastName") + .phoneNumber("123456789010") + .userType(UserType.ADMIN) + .build(); + + + User mockAdminWithCreate = User.builder() + .id(UUID.randomUUID().toString()) + .email(request.getEmail()) + .firstName(request.getFirstName()) + .lastName(request.getLastName()) + .phoneNumber(request.getPhoneNumber()) + .userType(request.getUserType()) + .build(); + + // When + when(registerService.registerUser(any(RegisterRequest.class))).thenReturn(mockAdminWithCreate); + + // Then + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/authentication/user/register") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(CustomResponse.SUCCESS))); + + // Verify + verify(registerService, times(1)).registerUser(any(RegisterRequest.class)); + + } + + @Test + void givenLoginRequestWithAdminCreate_WhenLoginForAdmin_ThenReturnToken() throws Exception { + + // Given + LoginRequest loginRequest = LoginRequest.builder() + .email("admin@example.com") + .password("password") + .build(); + + Token mockToken = Token.builder() + .accessToken("mockAccessToken") + .accessTokenExpiresAt(3600L) + .refreshToken("mockRefreshToken") + .build(); + + TokenResponse expectedTokenResponse = tokenToTokenResponseMapper.map(mockToken); + + // When + when(loginService.login(any(LoginRequest.class))).thenReturn(mockToken); + + // Then + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/authentication/user/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequest))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.accessToken").value(expectedTokenResponse.getAccessToken())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.accessTokenExpiresAt").value(expectedTokenResponse.getAccessTokenExpiresAt())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.refreshToken").value(expectedTokenResponse.getRefreshToken())); + + // Verify + verify(loginService, times(1)).login(any(LoginRequest.class)); + + } + + + @Test + void givenTokenRefreshRequestWithAdminCreate_WhenRefreshTokenForAdmin_ThenReturnTokenResponse() throws Exception { + + // Given + TokenRefreshRequest tokenRefreshRequest = new TokenRefreshRequest("refreshToken"); + + Token mockToken = Token.builder() + .accessToken("mockAccessToken") + .accessTokenExpiresAt(3600L) + .refreshToken("mockRefreshToken") + .build(); + + TokenResponse expectedTokenResponse = tokenToTokenResponseMapper.map(mockToken); + + // When + when(refreshTokenService.refreshToken(any(TokenRefreshRequest.class))).thenReturn(mockToken); + + // Then + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/authentication/user/refresh-token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(tokenRefreshRequest))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.accessToken").value(expectedTokenResponse.getAccessToken())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.accessTokenExpiresAt").value(expectedTokenResponse.getAccessTokenExpiresAt())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.refreshToken").value(expectedTokenResponse.getRefreshToken())); + + // Verify + verify(refreshTokenService, times(1)).refreshToken(any(TokenRefreshRequest.class)); + + } + + + @Test + void givenTokenInvalidateRequestWithAdminCreate_WhenLogoutForAdmin_ThenReturnInvalidateToken() throws Exception { + + // Given + TokenInvalidateRequest tokenInvalidateRequest = TokenInvalidateRequest.builder() + .accessToken("Bearer " + mockAdminToken.getAccessToken()) + .refreshToken(mockAdminToken.getRefreshToken()) + .build(); + + // When + doNothing().when(logoutService).logout(any(TokenInvalidateRequest.class)); + + // When + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/authentication/user/logout") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(tokenInvalidateRequest))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(CustomResponse.SUCCESS))); + + // Verify + verify(logoutService, times(1)).logout(any(TokenInvalidateRequest.class)); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/RoleNotFoundExceptionTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/RoleNotFoundExceptionTest.java new file mode 100644 index 000000000..553cadf90 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/RoleNotFoundExceptionTest.java @@ -0,0 +1,20 @@ +package com.example.demo.auth.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for verifying the behavior of the {@link RoleNotFoundException}. + * It checks the default constructor to ensure the correct error message is set. + */ +class RoleNotFoundExceptionTest { + + @Test + void testConstructor_DefaultMessage() { + RoleNotFoundException exception = new RoleNotFoundException(); + + assertEquals("Role not found!\n", exception.getMessage()); + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/TokenAlreadyInvalidatedExceptionTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/TokenAlreadyInvalidatedExceptionTest.java new file mode 100644 index 000000000..222092bb2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/TokenAlreadyInvalidatedExceptionTest.java @@ -0,0 +1,20 @@ +package com.example.demo.auth.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for verifying the behavior of the {@link TokenAlreadyInvalidatedException}. + * It checks the default constructor to ensure the correct error message is set. + */ +class TokenAlreadyInvalidatedExceptionTest { + + @Test + void testConstructor_DefaultMessage() { + TokenAlreadyInvalidatedException exception = new TokenAlreadyInvalidatedException(); + + assertEquals("Token is already invalidated!\n", exception.getMessage()); + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/UserAlreadyExistExceptionTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/UserAlreadyExistExceptionTest.java new file mode 100644 index 000000000..4f986becf --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/UserAlreadyExistExceptionTest.java @@ -0,0 +1,20 @@ +package com.example.demo.auth.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for verifying the behavior of the {@link UserAlreadyExistException}. + * It checks the default constructor to ensure the correct error message is set. + */ +class UserAlreadyExistExceptionTest { + + @Test + void testConstructor_DefaultMessage() { + UserAlreadyExistException exception = new UserAlreadyExistException(); + + assertEquals("User already exist!\n", exception.getMessage()); + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/UserStatusNotValidExceptionTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/UserStatusNotValidExceptionTest.java new file mode 100644 index 000000000..1b742331e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/exception/UserStatusNotValidExceptionTest.java @@ -0,0 +1,20 @@ +package com.example.demo.auth.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for verifying the behavior of the {@link UserStatusNotValidException}. + * It checks the default constructor to ensure the correct error message is set. + */ +class UserStatusNotValidExceptionTest { + + @Test + void testConstructor_DefaultMessage() { + UserStatusNotValidException exception = new UserStatusNotValidException(); + + assertEquals("User status is not valid!\n", exception.getMessage()); + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/filter/CustomBearerTokenAuthenticationFilterTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/filter/CustomBearerTokenAuthenticationFilterTest.java new file mode 100644 index 000000000..7e6149129 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/filter/CustomBearerTokenAuthenticationFilterTest.java @@ -0,0 +1,96 @@ +package com.example.demo.auth.filter; + +import com.example.demo.auth.service.InvalidTokenService; +import com.example.demo.auth.service.TokenService; +import com.example.demo.base.AbstractBaseServiceTest; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class CustomBearerTokenAuthenticationFilterTest extends AbstractBaseServiceTest { + + @InjectMocks + private CustomBearerTokenAuthenticationFilter customBearerTokenAuthenticationFilter; + + @Mock + private TokenService tokenService; + + @Mock + private InvalidTokenService invalidTokenService; + + @BeforeEach + void setup() { + SecurityContextHolder.clearContext(); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void shouldAuthenticateWithValidBearerToken() throws Exception { + + // Given + String validJwt = "valid-jwt-token"; + String tokenId = "valid-token-id"; + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + // When + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + validJwt); + when(tokenService.getId(validJwt)).thenReturn(tokenId); + when(tokenService.getAuthentication(validJwt)).thenReturn(new UsernamePasswordAuthenticationToken("user", null)); + + // Then + customBearerTokenAuthenticationFilter.doFilterInternal(request, response, filterChain); + + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + assertEquals("user", SecurityContextHolder.getContext().getAuthentication().getName()); + + // Verify + verify(tokenService).verifyAndValidate(validJwt); + verify(invalidTokenService).checkForInvalidityOfToken(tokenId); + verify(tokenService).getAuthentication(validJwt); + verify(filterChain).doFilter(request, response); + + } + + @Test + void shouldNotAuthenticateWithoutBearerToken() throws Exception { + + // Given + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + // When + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(null); + + // Then + customBearerTokenAuthenticationFilter.doFilterInternal(request, response, filterChain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + + // Verify + verify(tokenService, never()).verifyAndValidate(anyString()); + verify(invalidTokenService, never()).checkForInvalidityOfToken(any()); + verify(tokenService, never()).getAuthentication(any()); + verify(filterChain).doFilter(request, response); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/RegisterRequestToUserEntityMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/RegisterRequestToUserEntityMapperTest.java new file mode 100644 index 000000000..fd7826dc5 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/RegisterRequestToUserEntityMapperTest.java @@ -0,0 +1,95 @@ +package com.example.demo.auth.model.mapper; + +import com.example.demo.auth.model.dto.request.RegisterRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.enums.UserType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link RegisterRequestToUserEntityMapper}. + * This class tests the mapping functionality between a {@link RegisterRequest} and a + * {@link UserEntity} to ensure the correct transformation of data during user registration. + */ +class RegisterRequestToUserEntityMapperTest { + + private final RegisterRequestToUserEntityMapper mapper = RegisterRequestToUserEntityMapper.initialize(); + + @Test + void testMapRegisterRequestNull() { + + UserEntity result = mapper.map((RegisterRequest) null); + + assertNull(result); + + } + + @Test + void testMapRegisterRequestCollectionNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapRegisterRequestListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapRegisterRequestListWithNullElements() { + + List requests = Arrays.asList( + new RegisterRequest("test1@example.com", + "password1", + "UserFirstName", + "UserLastName", + "1234567890", + UserType.USER), + null + ); + + List result = mapper.map(requests); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNotNull(result.get(0)); + assertNull(result.get(1)); + + } + + @Test + void testMapSingleRegisterRequest() { + + RegisterRequest request = new RegisterRequest( + "test@example.com", "password", + "UserFirstName", "UserLastName", "1234567890", + UserType.ADMIN + ); + + UserEntity result = mapper.map(request); + + assertNotNull(result); + assertEquals(request.getEmail(), result.getEmail()); + assertEquals(request.getPassword(), result.getPassword()); + assertEquals(request.getFirstName(), result.getFirstName()); + assertEquals(request.getLastName(), result.getLastName()); + assertEquals(request.getPhoneNumber(), result.getPhoneNumber()); + assertEquals(request.getUserType(), result.getUserType()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/TokenToTokenResponseMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/TokenToTokenResponseMapperTest.java new file mode 100644 index 000000000..df5084a38 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/TokenToTokenResponseMapperTest.java @@ -0,0 +1,137 @@ +package com.example.demo.auth.model.mapper; + +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.response.TokenResponse; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link TokenToTokenResponseMapper}. + * This class tests the mapping functionality between a {@link Token} and a {@link TokenResponse} + * to ensure correct transformation of token data. + */ +class TokenToTokenResponseMapperTest { + + private final TokenToTokenResponseMapper mapper = TokenToTokenResponseMapper.initialize(); + + @Test + void testMapTokenNull() { + + TokenResponse result = mapper.map((Token) null); + + assertNull(result); + + } + + @Test + void testMapTokenCollectionNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapTokenListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapTokenListWithNullElements() { + + Token token = Token.builder() + .accessToken("access123") + .refreshToken("refresh123") + .accessTokenExpiresAt(123456789L) + .build(); + + List tokens = Arrays.asList(token, null); + + List result = mapper.map(tokens); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNotNull(result.get(0)); + assertNull(result.get(1)); + + } + + @Test + void testMapSingleToken() { + + Token token = Token.builder() + .accessToken("accessToken123") + .refreshToken("refreshToken123") + .accessTokenExpiresAt(987654321L) + .build(); + + TokenResponse result = mapper.map(token); + + assertNotNull(result); + assertEquals(token.getAccessToken(), result.getAccessToken()); + assertEquals(token.getRefreshToken(), result.getRefreshToken()); + assertEquals(token.getAccessTokenExpiresAt(), result.getAccessTokenExpiresAt()); + + } + + @Test + void testMapTokenListWithValues() { + + Token token1 = Token.builder() + .accessToken("access1") + .refreshToken("refresh1") + .accessTokenExpiresAt(111111111L) + .build(); + + Token token2 = Token.builder() + .accessToken("access2") + .refreshToken("refresh2") + .accessTokenExpiresAt(222222222L) + .build(); + + List tokens = Arrays.asList(token1, token2); + + List result = mapper.map(tokens); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(token1.getAccessToken(), result.get(0).getAccessToken()); + assertEquals(token2.getAccessToken(), result.get(1).getAccessToken()); + assertEquals(token1.getRefreshToken(), result.get(0).getRefreshToken()); + assertEquals(token2.getRefreshToken(), result.get(1).getRefreshToken()); + assertEquals(token1.getAccessTokenExpiresAt(), result.get(0).getAccessTokenExpiresAt()); + assertEquals(token2.getAccessTokenExpiresAt(), result.get(1).getAccessTokenExpiresAt()); + + } + + @Test + void testMapWithEdgeCaseValues() { + + Token token = Token.builder() + .accessToken("") + .refreshToken(null) + .accessTokenExpiresAt(null) + .build(); + + TokenResponse result = mapper.map(token); + + assertNotNull(result); + assertEquals("", result.getAccessToken()); + assertNull(result.getRefreshToken()); + assertNull(result.getAccessTokenExpiresAt()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/UserEntityToUserMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/UserEntityToUserMapperTest.java new file mode 100644 index 000000000..63836ad5a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/model/mapper/UserEntityToUserMapperTest.java @@ -0,0 +1,177 @@ +package com.example.demo.auth.model.mapper; + +import com.example.demo.auth.model.User; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.enums.UserStatus; +import com.example.demo.auth.model.enums.UserType; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link UserEntityToUserMapper}. + * This class tests the mapping functionality between a {@link UserEntity} and a {@link User} + * to ensure proper data transformation from entity to domain model. + */ +class UserEntityToUserMapperTest { + + private final UserEntityToUserMapper mapper = UserEntityToUserMapper.initialize(); + + @Test + void testMapUserEntityNull() { + + User result = mapper.map((UserEntity) null); + + assertNull(result); + + } + + @Test + void testMapUserEntityCollectionNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapUserEntityListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapUserEntityListWithNullElements() { + + UserEntity userEntity = UserEntity.builder() + .id(UUID.randomUUID().toString()) + .email("test@example.com") + .firstName("UserFirstName") + .lastName("UserLastName") + .phoneNumber("1234567890") + .userType(UserType.USER) + .userStatus(UserStatus.ACTIVE) + .build(); + + List userEntities = Arrays.asList(userEntity, null); + + List result = mapper.map(userEntities); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNotNull(result.get(0)); + assertNull(result.get(1)); + + } + + @Test + void testMapSingleUserEntity() { + + UserEntity userEntity = UserEntity.builder() + .id(UUID.randomUUID().toString()) + .email("test@example.com") + .firstName("UserFirstName") + .lastName("UserLastName") + .phoneNumber("1234567890") + .userType(UserType.USER) + .userStatus(UserStatus.ACTIVE) + .build(); + + User result = mapper.map(userEntity); + + assertNotNull(result); + assertEquals(userEntity.getId(), result.getId()); + assertEquals(userEntity.getEmail(), result.getEmail()); + assertEquals(userEntity.getFirstName(), result.getFirstName()); + assertEquals(userEntity.getLastName(), result.getLastName()); + assertEquals(userEntity.getPhoneNumber(), result.getPhoneNumber()); + assertEquals(userEntity.getUserType(), result.getUserType()); + assertEquals(userEntity.getUserStatus(), result.getUserStatus()); + assertEquals(userEntity.getCreatedAt(), result.getCreatedAt()); + assertEquals(userEntity.getCreatedBy(), result.getCreatedBy()); + assertEquals(userEntity.getUpdatedAt(), result.getUpdatedAt()); + assertEquals(userEntity.getUpdatedBy(), result.getUpdatedBy()); + + } + + @Test + void testMapUserEntityListWithValues() { + + UserEntity userEntity1 = UserEntity.builder() + .id(UUID.randomUUID().toString()) + .email("user1@example.com") + .firstName("UserFirstName1") + .lastName("UserLastName1") + .phoneNumber("1111111111") + .userType(UserType.USER) + .userStatus(UserStatus.ACTIVE) + .build(); + + UserEntity userEntity2 = UserEntity.builder() + .id(UUID.randomUUID().toString()) + .email("user2@example.com") + .firstName("UserFirstName2") + .lastName("UserLastName2") + .phoneNumber("2222222222") + .userType(UserType.USER) + .userStatus(UserStatus.ACTIVE) + .build(); + + List userEntities = Arrays.asList(userEntity1, userEntity2); + + List result = mapper.map(userEntities); + + assertNotNull(result); + assertEquals(2, result.size()); + + assertEquals(userEntity1.getId(), result.get(0).getId()); + assertEquals(userEntity1.getEmail(), result.get(0).getEmail()); + assertEquals(userEntity1.getFirstName(), result.get(0).getFirstName()); + + assertEquals(userEntity2.getId(), result.get(1).getId()); + assertEquals(userEntity2.getEmail(), result.get(1).getEmail()); + assertEquals(userEntity2.getFirstName(), result.get(1).getFirstName()); + } + + @Test + void testMapWithEdgeCaseValues() { + + UserEntity userEntity = UserEntity.builder() + .id(null) + .email("") + .firstName(null) + .lastName("") + .phoneNumber(null) + .userType(null) + .userStatus(null) + .createdAt(null) + .createdBy(null) + .updatedAt(null) + .updatedBy(null) + .build(); + + User result = mapper.map(userEntity); + + assertNotNull(result); + assertNull(result.getId()); + assertEquals("", result.getEmail()); + assertNull(result.getFirstName()); + assertEquals("", result.getLastName()); + assertNull(result.getPhoneNumber()); + assertNull(result.getUserType()); + assertNull(result.getUserStatus()); + assertNull(result.getCreatedAt()); + assertNull(result.getCreatedBy()); + assertNull(result.getUpdatedAt()); + assertNull(result.getUpdatedBy()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/security/CustomAuthenticationEntryPointTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/security/CustomAuthenticationEntryPointTest.java new file mode 100644 index 000000000..47a8a230c --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/security/CustomAuthenticationEntryPointTest.java @@ -0,0 +1,57 @@ +package com.example.demo.auth.security; + +import com.example.demo.base.AbstractBaseServiceTest; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; + +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class CustomAuthenticationEntryPointTest extends AbstractBaseServiceTest { + + @Mock + private HttpServletRequest httpServletRequest; + + @Mock + private HttpServletResponse httpServletResponse; + + @Mock + private AuthenticationException authenticationException; + + @Test + void shouldWriteCustomErrorResponseWhenAuthenticationFails() throws IOException { + + // Given + CustomAuthenticationEntryPoint customAuthenticationEntryPoint = new CustomAuthenticationEntryPoint(); + + // Mock behavior of HttpServletResponse + ServletOutputStream mockOutputStream = mock(ServletOutputStream.class); + when(httpServletResponse.getOutputStream()).thenReturn(mockOutputStream); // Mock getOutputStream() + + doNothing().when(httpServletResponse).setContentType(MediaType.APPLICATION_JSON_VALUE); + doNothing().when(httpServletResponse).setStatus(HttpStatus.UNAUTHORIZED.value()); + + // When + customAuthenticationEntryPoint.commence(httpServletRequest, httpServletResponse, authenticationException); + + // Then + // Verify that setContentType is called with the correct content type + verify(httpServletResponse).setContentType(MediaType.APPLICATION_JSON_VALUE); + + // Verify that setStatus is called with 401 Unauthorized status code + verify(httpServletResponse).setStatus(HttpStatus.UNAUTHORIZED.value()); + + // Verify that the output stream's write method is called with a non-empty response body + verify(mockOutputStream).write(any(byte[].class)); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/InvalidTokenServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/InvalidTokenServiceImplTest.java new file mode 100644 index 000000000..30a046dd3 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/InvalidTokenServiceImplTest.java @@ -0,0 +1,85 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.TokenAlreadyInvalidatedException; +import com.example.demo.auth.model.entity.InvalidTokenEntity; +import com.example.demo.auth.repository.InvalidTokenRepository; +import com.example.demo.base.AbstractBaseServiceTest; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link InvalidTokenServiceImpl}. + * This test class verifies the functionality of the service responsible for managing invalid tokens. + * It mocks the {@link InvalidTokenRepository} to simulate interactions with the data layer. + */ +class InvalidTokenServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private InvalidTokenServiceImpl invalidTokenService; + + @Mock + private InvalidTokenRepository invalidTokenRepository; + + @Test + void invalidateTokens_ShouldSaveAllInvalidTokens() { + + // Given + Set tokenIds = Set.of("token-id-1", "token-id-2"); + + // When + when(invalidTokenRepository.saveAll(any(Set.class))).thenReturn(Collections.emptyList()); + + // Then + invalidTokenService.invalidateTokens(tokenIds); + + // Verify + verify(invalidTokenRepository, times(1)).saveAll(any(Set.class)); + + } + + @Test + void checkForInvalidityOfToken_ShouldThrowExceptionIfTokenInvalid() { + + // Given + String tokenId = "test-token-id"; + + // When + when(invalidTokenRepository.findByTokenId(tokenId)).thenReturn(Optional.of(new InvalidTokenEntity())); + + // Then + assertThrows(TokenAlreadyInvalidatedException.class, () -> { + invalidTokenService.checkForInvalidityOfToken(tokenId); + }); + + // Verify + verify(invalidTokenRepository, times(1)).findByTokenId(tokenId); + + } + + @Test + void checkForInvalidityOfToken_ShouldNotThrowExceptionIfTokenValid() { + + // Given + String tokenId = "test-token-id"; + + // When + when(invalidTokenRepository.findByTokenId(tokenId)).thenReturn(Optional.empty()); + + // Then + invalidTokenService.checkForInvalidityOfToken(tokenId); + + // Verify + verify(invalidTokenRepository, times(1)).findByTokenId(tokenId); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/LoginServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/LoginServiceImplTest.java new file mode 100644 index 000000000..2213c8407 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/LoginServiceImplTest.java @@ -0,0 +1,142 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.PasswordNotValidException; +import com.example.demo.auth.exception.UserNotFoundException; +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.request.LoginRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.repository.UserRepository; +import com.example.demo.auth.service.TokenService; +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AdminUserBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link LoginServiceImpl}. + * This test class ensures the correct behavior of the login service, which handles the user login + * process, including verifying credentials and generating authentication tokens. + * It mocks the {@link UserRepository}, {@link PasswordEncoder}, and {@link TokenService} + * to isolate the login logic from external dependencies. + */ +class LoginServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private LoginServiceImpl loginService; + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private TokenService tokenService; + + @Test + void login_ValidCredentials_ReturnsToken() { + + // Given + final LoginRequest loginRequest = LoginRequest.builder() + .email("test@example.com") + .password("password123") + .build(); + + final UserEntity adminEntity = new AdminUserBuilder().withValidFields().build(); + + final Token expectedToken = Token.builder() + .accessToken("mockAccessToken") + .accessTokenExpiresAt(123456789L) + .refreshToken("mockRefreshToken") + .build(); + + // When + when(userRepository.findUserEntityByEmail(loginRequest.getEmail())) + .thenReturn(Optional.of(adminEntity)); + + when(passwordEncoder.matches(loginRequest.getPassword(), adminEntity.getPassword())) + .thenReturn(true); + + when(tokenService.generateToken(adminEntity.getClaims())).thenReturn(expectedToken); + + Token actualToken = loginService.login(loginRequest); + + // Then + assertEquals(expectedToken.getAccessToken(), actualToken.getAccessToken()); + assertEquals(expectedToken.getRefreshToken(), actualToken.getRefreshToken()); + assertEquals(expectedToken.getAccessTokenExpiresAt(), actualToken.getAccessTokenExpiresAt()); + + // Verify + verify(userRepository).findUserEntityByEmail(loginRequest.getEmail()); + verify(passwordEncoder).matches(loginRequest.getPassword(), adminEntity.getPassword()); + verify(tokenService).generateToken(adminEntity.getClaims()); + + } + + @Test + void login_InvalidEmail_ThrowsAdminNotFoundException() { + + // Given + final LoginRequest loginRequest = LoginRequest.builder() + .email("nonexistent@example.com") + .password("password123") + .build(); + + // When + when(userRepository.findUserEntityByEmail(loginRequest.getEmail())) + .thenReturn(Optional.empty()); + + // Then + UserNotFoundException exception = assertThrows(UserNotFoundException.class, + () -> loginService.login(loginRequest)); + + assertEquals("User not found!\n " + loginRequest.getEmail(), exception.getMessage()); + + // Verify + verify(userRepository).findUserEntityByEmail(loginRequest.getEmail()); + verifyNoInteractions(passwordEncoder, tokenService); + + } + + @Test + void login_InvalidPassword_ThrowsPasswordNotValidException() { + + // Given + final LoginRequest loginRequest = LoginRequest.builder() + .email("test@example.com") + .password("invalidPassword") + .build(); + + final UserEntity adminEntity = UserEntity.builder() + .email(loginRequest.getEmail()) + .password("encodedPassword") + .build(); + + // When + when(userRepository.findUserEntityByEmail(loginRequest.getEmail())) + .thenReturn(Optional.of(adminEntity)); + + when(passwordEncoder.matches(loginRequest.getPassword(), adminEntity.getPassword())) + .thenReturn(false); + + // Then + PasswordNotValidException exception = assertThrows(PasswordNotValidException.class, + () -> loginService.login(loginRequest)); + + assertNotNull(exception); + + // Verify + verify(userRepository).findUserEntityByEmail(loginRequest.getEmail()); + verify(passwordEncoder).matches(loginRequest.getPassword(), adminEntity.getPassword()); + verifyNoInteractions(tokenService); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/LogoutServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/LogoutServiceImplTest.java new file mode 100644 index 000000000..2626f6dc2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/LogoutServiceImplTest.java @@ -0,0 +1,84 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.model.dto.request.TokenInvalidateRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.service.InvalidTokenService; +import com.example.demo.auth.service.TokenService; +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.TokenBuilder; +import com.example.demo.builder.UserBuilder; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link LogoutServiceImpl}. + * This test class validates the functionality of the logout service, which is responsible + * for handling the user logout process, including token invalidation. + * It mocks the {@link TokenService} and {@link InvalidTokenService} to verify the logout logic + * without actually interacting with the token management system. + */ +class LogoutServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private LogoutServiceImpl logoutService; + + @Mock + private TokenService tokenService; + + @Mock + private InvalidTokenService invalidTokenService; + + @Test + void givenAccessTokenAndRefreshToken_whenLogoutForAdmin_thenReturnLogout() { + + // Given + final UserEntity mockUserEntity = new UserBuilder().withValidFields().build(); + + final Claims mockAccessTokenClaims = TokenBuilder.getValidClaims( + mockUserEntity.getId(), + mockUserEntity.getFirstName() + ); + + final Claims mockRefreshTokenClaims = TokenBuilder.getValidClaims( + mockUserEntity.getId(), + mockUserEntity.getFirstName() + ); + + final String mockAccessTokenId = mockAccessTokenClaims.getId(); + final String mockRefreshTokenId = mockRefreshTokenClaims.getId(); + + + final String accessToken = "validAccessToken"; + final String refreshToken = "validRefreshToken"; + + final TokenInvalidateRequest tokenInvalidateRequest = TokenInvalidateRequest.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + // When + doNothing().when(tokenService).verifyAndValidate(Set.of(accessToken, refreshToken)); + when(tokenService.getPayload(accessToken)).thenReturn(mockAccessTokenClaims); + doNothing().when(invalidTokenService).checkForInvalidityOfToken(mockAccessTokenId); + when(tokenService.getPayload(refreshToken)).thenReturn(mockRefreshTokenClaims); + doNothing().when(invalidTokenService).checkForInvalidityOfToken(mockRefreshTokenId); + doNothing().when(invalidTokenService).invalidateTokens(Set.of(mockAccessTokenId, mockRefreshTokenId)); + + // Then + logoutService.logout(tokenInvalidateRequest); + + // Verify + verify(tokenService).verifyAndValidate(Set.of(accessToken, refreshToken)); + verify(tokenService, times(2)).getPayload(anyString()); + verify(invalidTokenService, times(2)).checkForInvalidityOfToken(anyString()); + verify(invalidTokenService).invalidateTokens(anySet()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/RefreshTokenServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/RefreshTokenServiceImplTest.java new file mode 100644 index 000000000..11a68561d --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/RefreshTokenServiceImplTest.java @@ -0,0 +1,170 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.UserNotFoundException; +import com.example.demo.auth.exception.UserStatusNotValidException; +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.dto.request.TokenRefreshRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.enums.UserStatus; +import com.example.demo.auth.repository.UserRepository; +import com.example.demo.auth.service.TokenService; +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AdminUserBuilder; +import com.example.demo.builder.TokenBuilder; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link RefreshTokenServiceImpl}. + * This test class ensures the correct functionality of the refresh token service, + * which manages the generation of new tokens when a valid refresh token is provided. + * It mocks the {@link UserRepository} and {@link TokenService} to isolate the refresh token logic. + */ +class RefreshTokenServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private RefreshTokenServiceImpl refreshTokenService; + + @Mock + private UserRepository userRepository; + + @Mock + private TokenService tokenService; + + @Test + void refreshToken_ValidRefreshToken_ReturnsToken() { + + // Given + final String refreshTokenString = "mockRefreshToken"; + final TokenRefreshRequest tokenRefreshRequest = TokenRefreshRequest.builder() + .refreshToken(refreshTokenString) + .build(); + + final UserEntity mockAdminUserEntity = new AdminUserBuilder().withValidFields().build(); + + final Claims mockClaims = TokenBuilder.getValidClaims( + mockAdminUserEntity.getId(), + mockAdminUserEntity.getFirstName() + ); + + final Token expectedToken = Token.builder() + .accessToken("mockAccessToken") + .accessTokenExpiresAt(123456789L) + .refreshToken("newMockRefreshToken") + .build(); + + doNothing().when(tokenService).verifyAndValidate(refreshTokenString); + when(tokenService.getPayload(refreshTokenString)).thenReturn(mockClaims); + when(userRepository.findById(anyString())).thenReturn(Optional.of(mockAdminUserEntity)); + when(tokenService.generateToken(mockAdminUserEntity.getClaims(), refreshTokenString)).thenReturn(expectedToken); + + // When + Token actualToken = refreshTokenService.refreshToken(tokenRefreshRequest); + + // Then + assertNotNull(actualToken); + assertEquals(expectedToken.getAccessToken(), actualToken.getAccessToken()); + assertEquals(expectedToken.getAccessTokenExpiresAt(), actualToken.getAccessTokenExpiresAt()); + assertEquals(expectedToken.getRefreshToken(), actualToken.getRefreshToken()); + + // Verify + verify(tokenService).verifyAndValidate(refreshTokenString); + verify(tokenService).getPayload(refreshTokenString); + verify(userRepository).findById(anyString()); + verify(tokenService).generateToken(mockAdminUserEntity.getClaims(), refreshTokenString); + + } + + @Test + void refreshToken_InvalidRefreshToken_ThrowsException() { + + // Given + final String refreshTokenString = "invalidRefreshToken"; + final TokenRefreshRequest tokenRefreshRequest = TokenRefreshRequest.builder() + .refreshToken(refreshTokenString) + .build(); + + // When + doThrow(RuntimeException.class).when(tokenService).verifyAndValidate(refreshTokenString); + + // Then + assertThrows(RuntimeException.class, + () -> refreshTokenService.refreshToken(tokenRefreshRequest)); + + // Verify + verify(tokenService).verifyAndValidate(refreshTokenString); + verifyNoInteractions(userRepository); + + } + + @Test + void refreshToken_AdminNotFound_ThrowsException() { + + // Given + final String refreshTokenString = "validRefreshToken"; + final TokenRefreshRequest tokenRefreshRequest = TokenRefreshRequest.builder() + .refreshToken(refreshTokenString) + .build(); + + final Claims mockClaims = TokenBuilder.getValidClaims("nonExistentAdminId", "John"); + + // When + doNothing().when(tokenService).verifyAndValidate(refreshTokenString); + when(tokenService.getPayload(refreshTokenString)).thenReturn(mockClaims); + when(userRepository.findById("nonExistentAdminId")).thenReturn(Optional.empty()); + + // Then + UserNotFoundException exception = assertThrows(UserNotFoundException.class, + () -> refreshTokenService.refreshToken(tokenRefreshRequest)); + + assertEquals(""" + User not found! + """, exception.getMessage()); + + // Verify + verify(tokenService).verifyAndValidate(refreshTokenString); + verify(tokenService).getPayload(refreshTokenString); + verify(userRepository).findById("nonExistentAdminId"); + + } + + @Test + void refreshToken_InactiveAdmin_ThrowsException() { + + // Given + String refreshTokenString = "validRefreshToken"; + TokenRefreshRequest tokenRefreshRequest = TokenRefreshRequest.builder() + .refreshToken(refreshTokenString) + .build(); + + UserEntity inactiveAdmin = new AdminUserBuilder().withValidFields().withUserStatus(UserStatus.PASSIVE).build(); + + Claims mockClaims = TokenBuilder.getValidClaims(inactiveAdmin.getId(), inactiveAdmin.getFirstName()); + + // When + doNothing().when(tokenService).verifyAndValidate(refreshTokenString); + when(tokenService.getPayload(refreshTokenString)).thenReturn(mockClaims); + when(userRepository.findById(inactiveAdmin.getId())).thenReturn(Optional.of(inactiveAdmin)); + + // Then + UserStatusNotValidException exception = assertThrows(UserStatusNotValidException.class, + () -> refreshTokenService.refreshToken(tokenRefreshRequest)); + + assertEquals("User status is not valid!\n UserStatus = PASSIVE", exception.getMessage()); + + // Verify + verify(tokenService).verifyAndValidate(refreshTokenString); + verify(tokenService).getPayload(refreshTokenString); + verify(userRepository).findById(inactiveAdmin.getId()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/RegisterServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/RegisterServiceImplTest.java new file mode 100644 index 000000000..4628bbde0 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/RegisterServiceImplTest.java @@ -0,0 +1,107 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.exception.UserAlreadyExistException; +import com.example.demo.auth.model.User; +import com.example.demo.auth.model.dto.request.RegisterRequest; +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.enums.UserType; +import com.example.demo.auth.model.mapper.RegisterRequestToUserEntityMapper; +import com.example.demo.auth.model.mapper.UserEntityToUserMapper; +import com.example.demo.auth.repository.UserRepository; +import com.example.demo.base.AbstractBaseServiceTest; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link RegisterServiceImpl}. + * This test class verifies the functionality of the registration service, + * which handles user registration, including password encoding and saving user data. + * It mocks the {@link UserRepository} and {@link PasswordEncoder} to test the registration logic + * independently of external systems. + */ +class RegisterServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private RegisterServiceImpl registerService; + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + private final RegisterRequestToUserEntityMapper registerRequestToUserEntityMapper = + RegisterRequestToUserEntityMapper.initialize(); + + private final UserEntityToUserMapper userEntityToUserMapper = UserEntityToUserMapper.initialize(); + + @Test + void givenAdminRegisterRequest_whenRegisterAdmin_thenReturnAdmin() { + + // Given + final RegisterRequest request = RegisterRequest.builder() + .email("usertest@example.com") + .password("password123") + .firstName("User FirstName") + .lastName("User LastName") + .userType(UserType.USER) + .phoneNumber("1234567890") + .build(); + + final String encodedPassword = "encodedPassword"; + + final UserEntity userEntity = registerRequestToUserEntityMapper.mapForSaving(request); + + final User expected = userEntityToUserMapper.map(userEntity); + + // When + when(userRepository.existsUserEntityByEmail(request.getEmail())).thenReturn(false); + when(passwordEncoder.encode(request.getPassword())).thenReturn(encodedPassword); + when(userRepository.save(any(UserEntity.class))).thenReturn(userEntity); + + // Then + User result = registerService.registerUser(request); + + assertEquals(expected.getId(), result.getId()); + assertEquals(expected.getEmail(), result.getEmail()); + assertEquals(expected.getPhoneNumber(), result.getPhoneNumber()); + assertEquals(expected.getFirstName(), result.getFirstName()); + assertEquals(expected.getLastName(), result.getLastName()); + + // Verify + verify(userRepository).save(any(UserEntity.class)); + + } + + @Test + void givenAdminRegisterRequest_whenEmailAlreadyExists_thenThrowAdminAlreadyExistException() { + + // Given + final RegisterRequest request = RegisterRequest.builder() + .email("usertest@example.com") + .password("password123") + .firstName("User FirstName") + .lastName("User LastName") + .userType(UserType.USER) + .phoneNumber("1234567890") + .build(); + + // When + when(userRepository.existsUserEntityByEmail(request.getEmail())).thenReturn(true); + + // Then + assertThrows(UserAlreadyExistException.class, () -> registerService.registerUser(request)); + + // Verify + verify(userRepository, never()).save(any(UserEntity.class)); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/TokenServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/TokenServiceImplTest.java new file mode 100644 index 000000000..78a2c43e1 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/service/impl/TokenServiceImplTest.java @@ -0,0 +1,220 @@ +package com.example.demo.auth.service.impl; + +import com.example.demo.auth.config.TokenConfigurationParameter; +import com.example.demo.auth.model.Token; +import com.example.demo.auth.service.InvalidTokenService; +import com.example.demo.base.AbstractBaseServiceTest; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; + +/** + * Unit test class for {@link TokenServiceImpl}. + * This test class validates the functionality of the token service, + * which is responsible for generating, validating, and managing tokens. + * It mocks the {@link TokenConfigurationParameter} and {@link InvalidTokenService} + * to test the token-related logic without dependencies on the actual configuration or token invalidation logic. + */ +class TokenServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private TokenServiceImpl tokenService; + + @Mock + private TokenConfigurationParameter tokenConfigurationParameter; + + @Mock + private InvalidTokenService invalidTokenService; + + @Test + void testGenerateTokenWithoutRefreshToken() throws Exception { + + // Given + Map claims = new HashMap<>(); + claims.put("USER_ID", "12345"); + + // Generate a valid RSA key pair for testing + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + PrivateKey privateKey = keyPair.getPrivate(); + + Mockito.when(tokenConfigurationParameter.getAccessTokenExpireMinute()).thenReturn(60); + Mockito.when(tokenConfigurationParameter.getRefreshTokenExpireDay()).thenReturn(7); + Mockito.when(tokenConfigurationParameter.getPrivateKey()).thenReturn(privateKey); + Mockito.when(tokenConfigurationParameter.getIssuer()).thenReturn("issuer"); + + // When + Token token = tokenService.generateToken(claims); + + assertNotNull(token, "Token should not be null"); + assertNotNull(token.getAccessToken(), "Access token should not be null"); + assertNotNull(token.getRefreshToken(), "Refresh token should not be null"); + + // Verify + Mockito.verify(tokenConfigurationParameter).getAccessTokenExpireMinute(); + Mockito.verify(tokenConfigurationParameter).getRefreshTokenExpireDay(); + Mockito.verify(tokenConfigurationParameter, Mockito.times(2)).getPrivateKey(); + + } + + @Test + void testGenerateTokenWithRefreshToken() { + + // Given + KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); + PrivateKey privateKey = keyPair.getPrivate(); + PublicKey publicKey = keyPair.getPublic(); + + String refreshToken = Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)) + .signWith(privateKey) // Use the private key for signing + .compact(); + + Map claims = new HashMap<>(); + claims.put("USER_ID", "12345"); + + // Mock the TokenConfigurationParameter to return the keys + Mockito.when(tokenConfigurationParameter.getPrivateKey()).thenReturn(privateKey); + Mockito.when(tokenConfigurationParameter.getPublicKey()).thenReturn(publicKey); + Mockito.when(tokenConfigurationParameter.getAccessTokenExpireMinute()).thenReturn(60); + Mockito.when(tokenConfigurationParameter.getIssuer()).thenReturn("issuer"); + Mockito.doNothing().when(invalidTokenService).checkForInvalidityOfToken(anyString()); + + + // When + Token token = tokenService.generateToken(claims, refreshToken); + + // Then + assertNotNull(token); + assertNotNull(token.getAccessToken()); + assertNotNull(token.getRefreshToken()); + assertNotNull(token.getAccessTokenExpiresAt()); + + // Verify + Mockito.verify(tokenConfigurationParameter).getAccessTokenExpireMinute(); + Mockito.verify(invalidTokenService).checkForInvalidityOfToken(anyString()); + + } + + @Test + void testGetClaims() { + + // Given + KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + String jwt = Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setIssuer("issuer") + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // 1 hour + .signWith(privateKey) + .compact(); + + // When + Mockito.when(tokenConfigurationParameter.getPublicKey()).thenReturn(publicKey); + + // Then + Jws claims = tokenService.getClaims(jwt); + + assertNotNull(claims, "Claims should not be null"); + assertEquals("issuer", claims.getBody().getIssuer(), "Issuer should match"); + assertEquals("subject", claims.getBody().getSubject(), "Subject should match"); + + // Verify + Mockito.verify(tokenConfigurationParameter).getPublicKey(); + + } + + @Test + void testGetPayload() { + + // Given + KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + String jwt = Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setIssuer("issuer") + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // 1 hour + .signWith(privateKey) + .compact(); + + // When + Mockito.when(tokenConfigurationParameter.getPublicKey()).thenReturn(publicKey); + + // Then + Claims payload = tokenService.getPayload(jwt); + + + assertNotNull(payload, "Payload should not be null"); + assertEquals("issuer", payload.getIssuer(), "Issuer should match"); + assertEquals("subject", payload.getSubject(), "Subject should match"); + + // Verify + Mockito.verify(tokenConfigurationParameter).getPublicKey(); + + } + + @Test + void testVerifyAndValidateSet() { + + // Given + KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + String jwt1 = Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setIssuer("issuer1") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // 1 hour + .signWith(privateKey) + .compact(); + + String jwt2 = Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setIssuer("issuer2") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // 1 hour + .signWith(privateKey) + .compact(); + + Set jwts = Set.of(jwt1, jwt2); + + // When + Mockito.when(tokenConfigurationParameter.getPublicKey()).thenReturn(publicKey); + + // Then + assertDoesNotThrow(() -> tokenService.verifyAndValidate(jwts), "All tokens should be valid"); + + // Verify + Mockito.verify(tokenConfigurationParameter, Mockito.times(2)).getPublicKey(); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/utils/KeyConverterTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/utils/KeyConverterTest.java new file mode 100644 index 000000000..2a3d3265b --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/auth/utils/KeyConverterTest.java @@ -0,0 +1,51 @@ +package com.example.demo.auth.utils; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link KeyConverter}. + * This test class ensures that the utility class {@link KeyConverter}, which is designed to be + * used statically and not instantiated, cannot be instantiated using reflection. + */ +class KeyConverterTest { + + @Test + void utilityClass_ShouldNotBeInstantiated() { + assertThrows(InvocationTargetException.class, () -> { + // Attempt to use reflection to create an instance of the utility class + java.lang.reflect.Constructor constructor = KeyConverter.class.getDeclaredConstructor(); + constructor.setAccessible(true); + constructor.newInstance(); + }); + } + + @Test + void testConvertPublicKeyRuntimeException() { + String invalidPublicKey = "InvalidPublicKey"; + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + KeyConverter.convertPublicKey(invalidPublicKey); + }); + + assertNotNull(exception); + assertTrue(exception.getCause() instanceof IOException); + } + + @Test + void testConvertPrivateKeyRuntimeException() { + String invalidPrivateKey = "InvalidPrivateKey"; + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + KeyConverter.convertPrivateKey(invalidPrivateKey); + }); + + assertNotNull(exception); + assertTrue(exception.getCause() instanceof IOException); + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractBaseServiceTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractBaseServiceTest.java new file mode 100644 index 000000000..f00b5d1a0 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractBaseServiceTest.java @@ -0,0 +1,18 @@ +package com.example.demo.base; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Abstract base test class for service layer tests. This class is extended by all service test classes + * and ensures that necessary test setups and configurations are in place. + * This class is annotated with {@link MockitoExtension} to enable Mockito support and + * with {@link MockitoSettings} to configure the strictness of mocks. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public abstract class AbstractBaseServiceTest { + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractRestControllerTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractRestControllerTest.java new file mode 100644 index 000000000..8d4f587b7 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractRestControllerTest.java @@ -0,0 +1,109 @@ +package com.example.demo.base; + +import com.example.demo.auth.config.TokenConfigurationParameter; +import com.example.demo.auth.model.Token; +import com.example.demo.auth.model.enums.TokenClaims; +import com.example.demo.builder.AdminUserBuilder; +import com.example.demo.builder.UserBuilder; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.shaded.org.apache.commons.lang3.time.DateUtils; + +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +/** + * Abstract base test class for REST controller tests. This class is extended by all controller test classes + * and provides a mock MVC setup, a Jackson ObjectMapper, and mock tokens for authentication. + * This class sets up the necessary configurations for testing REST endpoints and includes mock authorization tokens + * for admin and user roles. + */ +@SpringBootTest +@AutoConfigureMockMvc +public class AbstractRestControllerTest extends AbstractTestContainerConfiguration { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + protected Token mockAdminToken; + + protected Token mockUserToken; + + @Mock + private TokenConfigurationParameter tokenConfiguration; + + /** + * Initializes the mock tokens for admin and user roles before each test. + * This method creates new tokens with the required claims for testing. + */ + @BeforeEach + public void initializeAuth() { + + this.tokenConfiguration = new TokenConfigurationParameter(); + this.mockAdminToken = this.generate(new AdminUserBuilder().withValidFields().build().getClaims()); + this.mockUserToken = this.generate(new UserBuilder().withValidFields().build().getClaims()); + } + + /** + * Generates a {@link Token} using the given claims for both access and refresh tokens. + * + * @param claims the claims to be included in the token. + * @return a {@link Token} containing access and refresh tokens. + */ + private Token generate(Map claims) { + + final long currentTimeMillis = System.currentTimeMillis(); + + final Date tokenIssuedAt = new Date(currentTimeMillis); + + final Date accessTokenExpiresAt = DateUtils.addMinutes(new Date(currentTimeMillis), tokenConfiguration.getAccessTokenExpireMinute()); + + final String accessToken = Jwts.builder() + .header() + .add(TokenClaims.TYP.getValue(), OAuth2AccessToken.TokenType.BEARER.getValue()) + .and() + .id(UUID.randomUUID().toString()) + .issuer(tokenConfiguration.getIssuer()) + .issuedAt(tokenIssuedAt) + .expiration(accessTokenExpiresAt) + .signWith(tokenConfiguration.getPrivateKey()) + .claims(claims) + .compact(); + + final Date refreshTokenExpiresAt = DateUtils.addDays(new Date(currentTimeMillis), tokenConfiguration.getRefreshTokenExpireDay()); + + final JwtBuilder refreshTokenBuilder = Jwts.builder(); + + final String refreshToken = refreshTokenBuilder + .header() + .add(TokenClaims.TYP.getValue(), OAuth2AccessToken.TokenType.BEARER.getValue()) + .and() + .id(UUID.randomUUID().toString()) + .issuer(tokenConfiguration.getIssuer()) + .issuedAt(tokenIssuedAt) + .expiration(refreshTokenExpiresAt) + .signWith(tokenConfiguration.getPrivateKey()) + .claim(TokenClaims.USER_ID.getValue(), claims.get(TokenClaims.USER_ID.getValue())) + .compact(); + + return Token.builder() + .accessToken(accessToken) + .accessTokenExpiresAt(accessTokenExpiresAt.toInstant().getEpochSecond()) + .refreshToken(refreshToken) + .build(); + + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractTestContainerConfiguration.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractTestContainerConfiguration.java new file mode 100644 index 000000000..d3d0e3401 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/base/AbstractTestContainerConfiguration.java @@ -0,0 +1,38 @@ +package com.example.demo.base; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * Abstract base configuration class for tests using Docker containers. + * This class provides setup for a Mongodb container + * are created and indexed. + * This class uses the Testcontainers library to manage the lifecycle of a Couchbase container + * and ensures that the environment is properly prepared for integration tests. + */ +@Slf4j +@Testcontainers +public class AbstractTestContainerConfiguration { + + // Create a MongoDB container instance + static final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:latest") + .withExposedPorts(27017); + + static { + // Start the MongoDB container before all tests + mongoDBContainer.start(); + } + + @DynamicPropertySource + public static void overrideProps(DynamicPropertyRegistry dynamicPropertyRegistry) { + // Override MongoDB properties dynamically using the MongoDB container's details + dynamicPropertyRegistry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); + dynamicPropertyRegistry.add("spring.data.mongodb.host", mongoDBContainer::getHost); + dynamicPropertyRegistry.add("spring.data.mongodb.port", () -> mongoDBContainer.getMappedPort(27017)); + } + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AdminUserBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AdminUserBuilder.java new file mode 100644 index 000000000..94f44c0bd --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AdminUserBuilder.java @@ -0,0 +1,72 @@ +package com.example.demo.builder; + +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.enums.UserStatus; +import com.example.demo.auth.model.enums.UserType; + +import java.util.UUID; + +/** + * A builder class for creating instances of {@link UserEntity} with specific properties set. + * This builder is intended for creating instances of {@link UserEntity} with specific configurations. + */ +public class AdminUserBuilder extends BaseBuilder { + + + public AdminUserBuilder() { + super(UserEntity.class); + } + + public AdminUserBuilder withValidFields() { + return this + .withId(UUID.randomUUID().toString()) + .withEmail("adminexample@example.com") + .withPassword("adminpassword") + .withFirstName("Admin First Name") + .withLastName("Admin Last Name") + .withPhoneNumber("1234567890") + .withUserType(UserType.ADMIN) + .withUserStatus(UserStatus.ACTIVE); + } + + public AdminUserBuilder withId(String id) { + data.setId(id); + return this; + } + + public AdminUserBuilder withEmail(String email) { + data.setEmail(email); + return this; + } + + public AdminUserBuilder withPassword(String password) { + data.setPassword(password); + return this; + } + + public AdminUserBuilder withFirstName(String firstName) { + data.setFirstName(firstName); + return this; + } + + public AdminUserBuilder withLastName(String lastName) { + data.setLastName(lastName); + return this; + } + + public AdminUserBuilder withPhoneNumber(String phoneNumber) { + data.setPhoneNumber(phoneNumber); + return this; + } + + public AdminUserBuilder withUserType(UserType userType) { + data.setUserType(userType); + return this; + } + + public AdminUserBuilder withUserStatus(UserStatus userStatus) { + data.setUserStatus(userStatus); + return this; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AirportBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AirportBuilder.java new file mode 100644 index 000000000..c0a62e79f --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AirportBuilder.java @@ -0,0 +1,39 @@ +package com.example.demo.builder; + +import com.example.demo.flight.model.Airport; + +import java.util.UUID; + +/** + * A builder class for creating instances of {@link Airport} with specific properties set. + * This builder is intended for creating instances of {@link Airport} with specific configurations. + */ +public class AirportBuilder extends BaseBuilder{ + + public AirportBuilder() { super(Airport.class);} + + public Airport withValidFields(){ + return this + .withId(UUID.randomUUID().toString()) + .withName("Airport Name") + .withCityName("Airport City Name") + .build(); + } + + public AirportBuilder withId(String id){ + data.setId(id); + return this; + } + + public AirportBuilder withName(String name){ + data.setName(name); + return this; + } + + public AirportBuilder withCityName(String cityName){ + data.setCityName(cityName); + return this; + } + +} + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AirportEntityBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AirportEntityBuilder.java new file mode 100644 index 000000000..939b1b484 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/AirportEntityBuilder.java @@ -0,0 +1,38 @@ +package com.example.demo.builder; + +import com.example.demo.flight.model.entity.AirportEntity; + +import java.util.UUID; + +/** + * A builder class for creating instances of {@link AirportEntity} with specific properties set. + * This builder is intended for creating instances of {@link AirportEntity} with specific configurations. + */ +public class AirportEntityBuilder extends BaseBuilder{ + + public AirportEntityBuilder() { super(AirportEntity.class);} + + public AirportEntity withValidFields(){ + return this + .withId(UUID.randomUUID().toString()) + .withName("Airport Name") + .withCityName("Airport City Name") + .build(); + } + + public AirportEntityBuilder withId(String id){ + data.setId(id); + return this; + } + + public AirportEntityBuilder withName(String name){ + data.setName(name); + return this; + } + + public AirportEntityBuilder withCityName(String cityName){ + data.setCityName(cityName); + return this; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/BaseBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/BaseBuilder.java new file mode 100644 index 000000000..ede1a874e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/BaseBuilder.java @@ -0,0 +1,25 @@ +package com.example.demo.builder; + +import lombok.SneakyThrows; + +/** + * A generic base builder class that can be used to build objects of type {@code T}. + * The builder initializes the object through reflection and provides a {@link #build()} method + * to return the constructed instance. + * + * @param the type of object this builder creates + */ +public abstract class BaseBuilder { + + @SneakyThrows + public BaseBuilder(Class clazz) { + this.data = clazz.getDeclaredConstructor().newInstance(); + } + + T data; + + public T build() { + return data; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/CreateAirportRequestBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/CreateAirportRequestBuilder.java new file mode 100644 index 000000000..bceaefcaf --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/CreateAirportRequestBuilder.java @@ -0,0 +1,27 @@ +package com.example.demo.builder; + +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; + +/** + * A builder class for creating instances of {@link CreateAirportRequest} with specific properties set. + */ +public class CreateAirportRequestBuilder extends BaseBuilder { + + public CreateAirportRequestBuilder() { super(CreateAirportRequest.class);} + + public CreateAirportRequestBuilder withValidFields(){ + return this.withName("Airport Name") + .withCityName("Airport City Name"); + } + + public CreateAirportRequestBuilder withName(String name){ + data.setName(name); + return this; + } + + public CreateAirportRequestBuilder withCityName(String cityName){ + data.setCityName(cityName); + return this; + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/CreateFlightRequestBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/CreateFlightRequestBuilder.java new file mode 100644 index 000000000..1c776fa79 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/CreateFlightRequestBuilder.java @@ -0,0 +1,49 @@ +package com.example.demo.builder; + + +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; + +import java.time.LocalDateTime; + +/** + * A builder class for creating instances of {@link CreateFlightRequest} with specific properties set. + */ +public class CreateFlightRequestBuilder extends BaseBuilder { + + public CreateFlightRequestBuilder() { super(CreateFlightRequest.class);} + + public CreateFlightRequestBuilder withValidFields() { + return this.withFromAirportId("AIRPORT1") + .withToAirportId("AIRPORT2") + .withDepartureTime(LocalDateTime.of(2025, 1, 13, 10, 0)) + .withArrivalTime(LocalDateTime.of(2025, 1, 13, 12, 0)) + .withPrice(100.0); + } + + public CreateFlightRequestBuilder withFromAirportId(String fromAirportId) { + data.setFromAirportId(fromAirportId); + return this; + } + + public CreateFlightRequestBuilder withToAirportId(String toAirportId) { + data.setToAirportId(toAirportId); + return this; + } + + public CreateFlightRequestBuilder withDepartureTime(LocalDateTime departureTime) { + data.setDepartureTime(departureTime); + return this; + } + + public CreateFlightRequestBuilder withArrivalTime(LocalDateTime arrivalTime) { + data.setArrivalTime(arrivalTime); + return this; + } + + public CreateFlightRequestBuilder withPrice(Double price) { + data.setPrice(price); + return this; + } + + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/FlightBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/FlightBuilder.java new file mode 100644 index 000000000..a1060b033 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/FlightBuilder.java @@ -0,0 +1,69 @@ +package com.example.demo.builder; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.Flight; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * A builder class for creating instances of {@link Flight} with specific properties set. + * This builder is intended for creating instances of {@link Flight} with specific configurations. + */ +public class FlightBuilder extends BaseBuilder { + + public FlightBuilder() { + super(Flight.class); + } + + public FlightBuilder withValidFields() { + return this + .withId(UUID.randomUUID().toString()) + .withFromAirport(new AirportBuilder() + .withId(UUID.randomUUID().toString()) + .withName("Origin Airport") + .withCityName("Origin City") + .build()) + .withToAirport(new AirportBuilder() + .withId(UUID.randomUUID().toString()) + .withName("Destination Airport") + .withCityName("Destination City") + .build()) + .withDepartureTime(LocalDateTime.now().plusDays(1)) + .withArrivalTime(LocalDateTime.now().plusDays(1).plusHours(2)) + .withPrice(150.0); + + } + + public FlightBuilder withId(String id) { + data.setId(id); + return this; + } + + public FlightBuilder withFromAirport(Airport fromAirport) { + data.setFromAirport(fromAirport); + return this; + } + + public FlightBuilder withToAirport(Airport toAirport) { + data.setToAirport(toAirport); + return this; + } + + public FlightBuilder withDepartureTime(LocalDateTime departureTime) { + data.setDepartureTime(departureTime); + return this; + } + + public FlightBuilder withArrivalTime(LocalDateTime arrivalTime) { + data.setArrivalTime(arrivalTime); + return this; + } + + public FlightBuilder withPrice(Double price) { + data.setPrice(price); + return this; + } + +} + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/FlightEntityBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/FlightEntityBuilder.java new file mode 100644 index 000000000..cc3c562be --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/FlightEntityBuilder.java @@ -0,0 +1,68 @@ +package com.example.demo.builder; + +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * A builder class for creating instances of {@link FlightEntity} with specific properties set. + * This builder is intended for creating instances of {@link FlightEntity} with specific configurations. + */ +public class FlightEntityBuilder extends BaseBuilder { + + public FlightEntityBuilder() { + super(FlightEntity.class); + } + + public FlightEntityBuilder withValidFields() { + return this + .withId(UUID.randomUUID().toString()) + .withFromAirport(new AirportEntityBuilder() + .withId(UUID.randomUUID().toString()) + .withName("Origin Airport") + .withCityName("Origin City") + .build()) + .withToAirport(new AirportEntityBuilder() + .withId(UUID.randomUUID().toString()) + .withName("Destination Airport") + .withCityName("Destination City") + .build()) + .withDepartureTime(LocalDateTime.now().plusDays(1)) + .withArrivalTime(LocalDateTime.now().plusDays(1).plusHours(2)) + .withPrice(150.0); + } + + public FlightEntityBuilder withId(String id) { + data.setId(id); + return this; + } + + public FlightEntityBuilder withFromAirport(AirportEntity fromAirport) { + data.setFromAirport(fromAirport); + return this; + } + + public FlightEntityBuilder withToAirport(AirportEntity toAirport) { + data.setToAirport(toAirport); + return this; + } + + public FlightEntityBuilder withDepartureTime(LocalDateTime departureTime) { + data.setDepartureTime(departureTime); + return this; + } + + public FlightEntityBuilder withArrivalTime(LocalDateTime arrivalTime) { + data.setArrivalTime(arrivalTime); + return this; + } + + public FlightEntityBuilder withPrice(Double price) { + data.setPrice(price); + return this; + } + +} + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/TokenBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/TokenBuilder.java new file mode 100644 index 000000000..df158bd8c --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/TokenBuilder.java @@ -0,0 +1,32 @@ +package com.example.demo.builder; + +import com.example.demo.auth.model.enums.TokenClaims; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * A builder class for creating JWT claims used for token generation. + */ +public class TokenBuilder { + + /** + * Creates valid JWT claims for a given user ID and first name. + * These claims are typically used to generate a JWT token. + * + * @param userId the user ID to include in the claims + * @param firstName the user's first name to include in the claims + * @return the generated JWT claims + */ + public static Claims getValidClaims(String userId, String firstName) { + Map mockClaimsMap = new HashMap<>(); + mockClaimsMap.put(TokenClaims.JWT_ID.getValue(), UUID.randomUUID().toString()); + mockClaimsMap.put(TokenClaims.USER_ID.getValue(), userId); + mockClaimsMap.put(TokenClaims.USER_FIRST_NAME.getValue(), firstName); + return Jwts.claims(mockClaimsMap); + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UpdateAirportRequestBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UpdateAirportRequestBuilder.java new file mode 100644 index 000000000..5f995b872 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UpdateAirportRequestBuilder.java @@ -0,0 +1,27 @@ +package com.example.demo.builder; + +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; + +/** + * A builder class for creating instances of {@link UpdateAirportRequestBuilder} with specific properties set. + */ +public class UpdateAirportRequestBuilder extends BaseBuilder { + + public UpdateAirportRequestBuilder() { super(UpdateAirportRequest.class);} + + public UpdateAirportRequestBuilder withValidFields(){ + return this.withName("Airport Name") + .withCityName("Airport City Name"); + } + + public UpdateAirportRequestBuilder withName(String name){ + data.setName(name); + return this; + } + + public UpdateAirportRequestBuilder withCityName(String cityName){ + data.setCityName(cityName); + return this; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UpdateFlightRequestBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UpdateFlightRequestBuilder.java new file mode 100644 index 000000000..bc513a694 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UpdateFlightRequestBuilder.java @@ -0,0 +1,51 @@ +package com.example.demo.builder; + +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * A builder class for creating instances of {@link UpdateFlightRequestBuilder} with specific properties set. + */ +public class UpdateFlightRequestBuilder extends BaseBuilder { + + public UpdateFlightRequestBuilder() { + super(UpdateFlightRequest.class); + } + + public UpdateFlightRequestBuilder withValidFields() { + return this.withFromAirportId(UUID.randomUUID().toString()) + .withToAirportId(UUID.randomUUID().toString()) + .withDepartureTime(LocalDateTime.now().plusDays(1)) + .withArrivalTime(LocalDateTime.now().plusDays(2)) + .withPrice(250.0); + } + + public UpdateFlightRequestBuilder withFromAirportId(String fromAirportId) { + data.setFromAirportId(fromAirportId); + return this; + } + + public UpdateFlightRequestBuilder withToAirportId(String toAirportId) { + data.setToAirportId(toAirportId); + return this; + } + + public UpdateFlightRequestBuilder withDepartureTime(LocalDateTime departureTime) { + data.setDepartureTime(departureTime); + return this; + } + + public UpdateFlightRequestBuilder withArrivalTime(LocalDateTime arrivalTime) { + data.setArrivalTime(arrivalTime); + return this; + } + + public UpdateFlightRequestBuilder withPrice(Double price) { + data.setPrice(price); + return this; + } + +} + diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UserBuilder.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UserBuilder.java new file mode 100644 index 000000000..76a757dd0 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/builder/UserBuilder.java @@ -0,0 +1,70 @@ +package com.example.demo.builder; + +import com.example.demo.auth.model.entity.UserEntity; +import com.example.demo.auth.model.enums.UserStatus; +import com.example.demo.auth.model.enums.UserType; + +import java.util.UUID; + +/** + * A builder class for creating instances of {@link UserEntity} with specific properties set. + */ +public class UserBuilder extends BaseBuilder { + + public UserBuilder() { + super(UserEntity.class); + } + + public UserBuilder withValidFields() { + return this + .withId(UUID.randomUUID().toString()) + .withEmail("userexample@example.com") + .withPassword("userpassword") + .withFirstName("User FirstName") + .withLastName("User LastName") + .withPhoneNumber("1234567890") + .withUserType(UserType.USER) + .withUserStatus(UserStatus.ACTIVE); + } + + public UserBuilder withId(String id) { + data.setId(id); + return this; + } + + public UserBuilder withEmail(String email) { + data.setEmail(email); + return this; + } + + public UserBuilder withPassword(String password) { + data.setPassword(password); + return this; + } + + public UserBuilder withFirstName(String firstName) { + data.setFirstName(firstName); + return this; + } + + public UserBuilder withLastName(String lastName) { + data.setLastName(lastName); + return this; + } + + public UserBuilder withPhoneNumber(String phoneNumber) { + data.setPhoneNumber(phoneNumber); + return this; + } + + public UserBuilder withUserType(UserType userType) { + data.setUserType(userType); + return this; + } + + public UserBuilder withUserStatus(UserStatus userStatus) { + data.setUserStatus(userStatus); + return this; + } + +} diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/config/OpenApiConfigTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/config/OpenApiConfigTest.java new file mode 100644 index 000000000..d3fca91f6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/config/OpenApiConfigTest.java @@ -0,0 +1,62 @@ +package com.example.demo.common.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for verifying the configuration of OpenAPI documentation. + */ +class OpenApiConfigTest { + + @Test + void openApiInfo() { + + // Given + OpenAPIDefinition openAPIDefinition = OpenApiConfig.class.getAnnotation(OpenAPIDefinition.class); + + // Then + assertEquals("1.0.0", openAPIDefinition.info().version()); + assertEquals("flightsearchapi", openAPIDefinition.info().title()); + assertEquals("Case Study - Flight Search Api" + + " (Java 21, Spring Boot, Mongodb, JUnit, Spring Security, JWT, Docker, Kubernetes, Prometheus, Grafana, Github Actions (CI/CD) ) ", + openAPIDefinition.info().description()); + + } + + @Test + void securityScheme() { + + // Given + SecurityScheme securityScheme = OpenApiConfig.class.getAnnotation(SecurityScheme.class); + + // Then + assertEquals("bearerAuth", securityScheme.name()); + assertEquals("JWT Token", securityScheme.description()); + assertEquals("bearer", securityScheme.scheme()); + assertEquals(SecuritySchemeType.HTTP, securityScheme.type()); + assertEquals("JWT", securityScheme.bearerFormat()); + assertEquals(SecuritySchemeIn.HEADER, securityScheme.in()); + + } + + @Test + void contactInfo() { + + // Given + Info info = OpenApiConfig.class.getAnnotation(OpenAPIDefinition.class).info(); + Contact contact = info.contact(); + + // Then + assertEquals("Sercan Noyan Germiyanoğlu", contact.name()); + assertEquals("https://github.com/Rapter1990/flightsearchapi", contact.url()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/exception/GlobalExceptionHandlerTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 000000000..778c2d0d9 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,338 @@ +package com.example.demo.common.exception; + +import com.example.demo.auth.exception.*; +import com.example.demo.base.AbstractRestControllerTest; +import com.example.demo.common.model.CustomError; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test class for the {@link GlobalExceptionHandler}, ensuring proper handling and response + * for various exceptions in the application. + */ +class GlobalExceptionHandlerTest extends AbstractRestControllerTest { + + @InjectMocks + private GlobalExceptionHandler globalExceptionHandler; + + @Test + void givenMethodArgumentNotValidException_whenHandleMethodArgumentNotValid_thenRespondWithBadRequest() { + + // Given + BindingResult bindingResult = mock(BindingResult.class); + MethodArgumentNotValidException ex = new MethodArgumentNotValidException(null, bindingResult); + FieldError fieldError = new FieldError("objectName", "fieldName", "error message"); + List objectErrors = Collections.singletonList(fieldError); + + when(bindingResult.getAllErrors()).thenReturn(objectErrors); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message("Validation failed") + .subErrors(Collections.singletonList( + CustomError.CustomSubError.builder() + .field("fieldName") + .message("error message") + .build())) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleMethodArgumentNotValid(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + CustomError actualError = (CustomError) responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenConstraintViolationException_whenHandlePathVariableErrors_thenRespondWithBadRequest() { + + // Given + ConstraintViolation mockViolation = mock(ConstraintViolation.class); + Path mockPath = mock(Path.class); + Set> violations = Set.of(mockViolation); + ConstraintViolationException mockException = new ConstraintViolationException(violations); + + CustomError.CustomSubError subError = CustomError.CustomSubError.builder() + .message("must not be null") + .field("") + .value("invalid value") + .type("String") // Default to String if getRootBeanClass() is null + .build(); + + CustomError expectedError = CustomError.builder() + .time(LocalDateTime.now()) + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message("Constraint violation") + .subErrors(Collections.singletonList(subError)) + .build(); + + // When + when(mockViolation.getMessage()).thenReturn("must not be null"); + when(mockViolation.getPropertyPath()).thenReturn(mockPath); + when(mockPath.toString()).thenReturn("field"); + when(mockViolation.getInvalidValue()).thenReturn("invalid value"); + when(mockViolation.getRootBeanClass()).thenReturn(String.class); // Ensure this does not return null + + // Then + ResponseEntity responseEntity = globalExceptionHandler.handlePathVariableErrors(mockException); + + CustomError actualError = (CustomError) responseEntity.getBody(); + + // Verify + checkCustomError(expectedError, actualError); + + } + + @Test + void givenRuntimeException_whenHandleRuntimeException_thenRespondWithNotFound() { + + // Given + RuntimeException ex = new RuntimeException("Runtime exception message"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.NOT_FOUND) + .header(CustomError.Header.API_ERROR.getName()) + .message("Runtime exception message") + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleRuntimeException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + + CustomError actualError = (CustomError) responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenAccessDeniedException_whenHandleAccessDeniedException_thenRespondWithForbidden() { + + // Given + AccessDeniedException ex = new AccessDeniedException("Access denied message"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.FORBIDDEN) + .header(CustomError.Header.AUTH_ERROR.getName()) + .message("Access denied message") + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleAccessDeniedException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + CustomError actualError = (CustomError) responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenPasswordNotValidException_whenHandlePasswordNotValidException_thenRespondWithBadRequest() { + + // Given + PasswordNotValidException ex = new PasswordNotValidException("Password not valid message"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message("Password is not valid!\n Password not valid message") + .isSuccess(false) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handlePasswordNotValidException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + CustomError actualError = responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenRoleNotFoundException_whenHandleRoleNotFoundException_thenRespondWithNotFound() { + + // Given + RoleNotFoundException ex = new RoleNotFoundException("Role not found message"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.NOT_FOUND) + .header(CustomError.Header.NOT_FOUND.getName()) + .message("Role not found!\n Role not found message") + .isSuccess(false) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleRoleNotFoundException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + CustomError actualError = responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenTokenAlreadyInvalidatedException_whenHandleTokenAlreadyInvalidatedException_thenRespondWithBadRequest() { + + // Given + TokenAlreadyInvalidatedException ex = new TokenAlreadyInvalidatedException("Token already invalidated"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message("Token is already invalidated!\n TokenID = Token already invalidated") + .isSuccess(false) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleTokenAlreadyInvalidatedException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + CustomError actualError = responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenUserAlreadyExistException_whenHandleUserAlreadyExistException_thenRespondWithConflict() { + + // Given + UserAlreadyExistException ex = new UserAlreadyExistException("User already exists"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.CONFLICT) + .header(CustomError.Header.ALREADY_EXIST.getName()) + .message("User already exist!\n User already exists") + .isSuccess(false) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleUserAlreadyExistException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + CustomError actualError = responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenUserNotFoundException_whenHandleUserNotFoundException_thenRespondWithNotFound() { + + // Given + UserNotFoundException ex = new UserNotFoundException("User not found"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.NOT_FOUND) + .header(CustomError.Header.NOT_FOUND.getName()) + .message("User not found!\n User not found") + .isSuccess(false) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleUserNotFoundException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + CustomError actualError = responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenUserStatusNotValidException_whenHandleUserStatusNotValidException_thenRespondWithBadRequest() { + + // Given + UserStatusNotValidException ex = new UserStatusNotValidException("User status not valid"); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.BAD_REQUEST) + .header(CustomError.Header.VALIDATION_ERROR.getName()) + .message("User status is not valid!\n User status not valid") + .isSuccess(false) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleUserStatusNotValidException(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + CustomError actualError = responseEntity.getBody(); + checkCustomError(expectedError, actualError); + + } + + @Test + void givenUnAuthorizeAttemptException_whenHandleUnAuthorizeAttempt_thenRespondWithUnauthorized() { + // Given + UnAuthorizeAttemptException ex = new UnAuthorizeAttemptException(); + + CustomError expectedError = CustomError.builder() + .httpStatus(HttpStatus.UNAUTHORIZED) + .header(CustomError.Header.AUTH_ERROR.getName()) + .message("You do not have permission to create a to-do item.\n") + .isSuccess(false) + .build(); + + // When + ResponseEntity responseEntity = globalExceptionHandler.handleUnAuthorizeAttempt(ex); + + // Then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + CustomError actualError = (CustomError) responseEntity.getBody(); // Cast response body + checkCustomError(expectedError, actualError); + + } + + private void checkCustomError(CustomError expectedError, CustomError actualError) { + + assertThat(actualError).isNotNull(); + assertThat(actualError.getTime()).isNotNull(); + assertThat(actualError.getHeader()).isEqualTo(expectedError.getHeader()); + assertThat(actualError.getIsSuccess()).isEqualTo(expectedError.getIsSuccess()); + + if (expectedError.getMessage() != null) { + assertThat(actualError.getMessage()).isEqualTo(expectedError.getMessage()); + } + + if (expectedError.getSubErrors() != null) { + assertThat(actualError.getSubErrors().size()).isEqualTo(expectedError.getSubErrors().size()); + if (!expectedError.getSubErrors().isEmpty()) { + assertThat(actualError.getSubErrors().get(0).getMessage()).isEqualTo(expectedError.getSubErrors().get(0).getMessage()); + assertThat(actualError.getSubErrors().get(0).getField()).isEqualTo(expectedError.getSubErrors().get(0).getField()); + assertThat(actualError.getSubErrors().get(0).getValue()).isEqualTo(expectedError.getSubErrors().get(0).getValue()); + assertThat(actualError.getSubErrors().get(0).getType()).isEqualTo(expectedError.getSubErrors().get(0).getType()); + } + } + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/model/dto/response/CustomPagingResponseTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/model/dto/response/CustomPagingResponseTest.java new file mode 100644 index 000000000..047cebffe --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/common/model/dto/response/CustomPagingResponseTest.java @@ -0,0 +1,39 @@ +package com.example.demo.common.model.dto.response; + +import com.example.demo.common.model.CustomPage; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit test class for {@link CustomPagingResponse}. + * This class tests the functionality of the {@link CustomPagingResponse}, focusing on the behavior + * of the static {@code of} method. The tests ensure that the method correctly creates an instance + * of {@link CustomPagingResponse} with the expected values based on the provided input data. + */ +class CustomPagingResponseTest { + + @Test + void testOfMethod() { + + // Given + CustomPage customPage = new CustomPage<>(); + customPage.setPageNumber(1); + customPage.setPageSize(10); + customPage.setTotalElementCount(100L); + customPage.setTotalPageCount(10); + + // Act: use the builder's 'of' method to create a CustomPagingResponse + CustomPagingResponse response = CustomPagingResponse.builder() + .of(customPage) + .build(); + + // Assert: verify that the fields from CustomPage are correctly mapped to the response + assertEquals(1, response.getPageNumber()); + assertEquals(10, response.getPageSize()); + assertEquals(100L, response.getTotalElementCount()); + assertEquals(10, response.getTotalPageCount()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/AirportControllerTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/AirportControllerTest.java new file mode 100644 index 000000000..a061e4e4a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/AirportControllerTest.java @@ -0,0 +1,655 @@ +package com.example.demo.flight.controller; + +import com.example.demo.base.AbstractRestControllerTest; +import com.example.demo.builder.AirportBuilder; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.builder.CreateAirportRequestBuilder; +import com.example.demo.builder.UpdateAirportRequestBuilder; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.AirportPagingRequest; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.model.dto.response.airport.AirportResponse; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.mapper.airport.AirportToAirportResponseMapper; +import com.example.demo.flight.model.mapper.airport.CustomPageAirportToCustomPagingAirportResponseMapper; +import com.example.demo.flight.service.airport.AirportService; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Integration test class for the {@link AirportController}. + * This class tests the REST endpoints of the {@link AirportController} to ensure that task-related + * operations are performed correctly. + */ +class AirportControllerTest extends AbstractRestControllerTest { + + @MockitoBean + AirportService airportService; + + private final AirportToAirportResponseMapper airportToAirportResponseMapper = + AirportToAirportResponseMapper.initialize(); + + private final CustomPageAirportToCustomPagingAirportResponseMapper customPageAirportToCustomPagingAirportResponseMapper + = CustomPageAirportToCustomPagingAirportResponseMapper.initialize(); + + @Test + void givenValidCreateAirportRequestByAdmin_whenCreateAirport_thenSuccess() throws Exception{ + + // Given + final CreateAirportRequest request = new CreateAirportRequestBuilder() + .withValidFields().build(); + + final Airport expectedAirport = Airport.builder() + .id(UUID.randomUUID().toString()) + .name(request.getName()) + .build(); + + // When + when(airportService.createAirport(any(CreateAirportRequest.class))).thenReturn(expectedAirport); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/v1/airports") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION,"Bearer " + mockAdminToken.getAccessToken()) + + ).andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response").value(expectedAirport.getId())); + + // Verify + verify(airportService,times(1)).createAirport(any(CreateAirportRequest.class)); + + } + + @Test + void givenValidCreateAirportRequest_WhenWithUserCreate_ThenThrowUnAuthorizeException() throws Exception{ + + // Given + final CreateAirportRequest request = new CreateAirportRequestBuilder() + .withValidFields().build(); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/v1/airports") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(airportService, never()).createAirport(any(CreateAirportRequest.class)); + + } + + @Test + void givenValidCreateAirportRequest_whenForbiddenThroughUser_thenThrowForbidden() throws Exception { + + // Given + final CreateAirportRequest request = new CreateAirportRequestBuilder() + .withValidFields().build(); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/v1/airports") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken()) + .content(objectMapper.writeValueAsString(request)) + ).andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + + // Verify + verify(airportService, never()).createAirport(any(CreateAirportRequest.class)); + + } + + @Test + void givenExistAirportId_whenGetAirportByIdWithAdmin_thenReturnCustomResponse() throws Exception{ + + // Given + final String mockAirportId = UUID.randomUUID().toString(); + + final Airport mockAirport = new AirportBuilder() + .withId(mockAirportId) + .withValidFields(); + + final AirportResponse expectedResponse = airportToAirportResponseMapper.map(mockAirport); + + // When + when(airportService.getAirportById(mockAirportId)).thenReturn(mockAirport); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/airports/{id}",mockAirportId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.id").value(expectedResponse.getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.name").value(expectedResponse.getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")); + + // Verify + verify(airportService, times(1)).getAirportById(mockAirportId); + + } + + @Test + void givenExistAirportId_whenGetAirportByIdWithUser_thenReturnCustomResponse() throws Exception{ + + // Given + final String mockAirportId = UUID.randomUUID().toString(); + + final Airport mockAirport = new AirportBuilder() + .withId(mockAirportId) + .withValidFields(); + + final AirportResponse expectedResponse = airportToAirportResponseMapper.map(mockAirport); + + // When + when(airportService.getAirportById(mockAirportId)).thenReturn(mockAirport); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/airports/{id}",mockAirportId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.id").value(expectedResponse.getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.name").value(expectedResponse.getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")); + + // Verify + verify(airportService, times(1)).getAirportById(mockAirportId); + + } + + @Test + void givenNonExistAirportId_whenGetAirportByIdWithAdmin_thenThrowAirportNotFoundException() throws Exception { + + // Given + final String nonExistentTaskId = UUID.randomUUID().toString(); + final String expectedMessage = "Airport not found!\n Airport not found with ID: " + nonExistentTaskId; + + // When + when(airportService.getAirportById(nonExistentTaskId)) + .thenThrow(new AirportNotFoundException("Airport not found with ID: " + nonExistentTaskId)); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/airports/{id}", nonExistentTaskId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.header").value("NOT EXIST")) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)); + + // Verify + verify(airportService, times(1)).getAirportById(nonExistentTaskId); + + } + + + @Test + void givenNonExistAirportId_whenGetAirportByIdWithUser_thenThrowAirportNotFoundException() throws Exception { + + // Given + final String nonExistentTaskId = UUID.randomUUID().toString(); + final String expectedMessage = "Airport not found!\n Airport not found with ID: " + nonExistentTaskId; + + // When + when(airportService.getAirportById(nonExistentTaskId)) + .thenThrow(new AirportNotFoundException("Airport not found with ID: " + nonExistentTaskId)); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/airports/{id}", nonExistentTaskId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.header").value("NOT EXIST")) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)); + + // Verify + verify(airportService, times(1)).getAirportById(nonExistentTaskId); + + } + + @Test + void givenExistAirportId_whenUserUnauthorized_thenReturnUnauthorized() throws Exception { + + // Given + final String mockAirportId = UUID.randomUUID().toString(); + + final Airport mockAirport = new AirportBuilder() + .withId(mockAirportId) + .withValidFields(); + + // When + when(airportService.getAirportById(mockAirportId)).thenReturn(mockAirport); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/airports/{id}",mockAirportId) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(airportService,never()).getAirportById(mockAirportId); + + } + + @Test + void givenAirportPagingRequest_whenGetAirportsFromAdmin_thenReturnCustomPageTask() throws Exception { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + final String airportId = UUID.randomUUID().toString(); + + final AirportEntity expectedEntity = new AirportEntityBuilder() + .withId(airportId) + .withValidFields(); + + final List airportEntities = List.of(expectedEntity); + + final Page airportEntityPage = new PageImpl<>(airportEntities, PageRequest.of(1, 1), airportEntities.size()); + + final List airportDomainModels = airportEntities.stream() + .map(entity -> new Airport(entity.getId(), entity.getName(), entity.getCityName())) + .collect(Collectors.toList()); + + final CustomPage taskPage = CustomPage.of(airportDomainModels, airportEntityPage); + + final CustomPagingResponse expectedResponse = + customPageAirportToCustomPagingAirportResponseMapper.toPagingResponse(taskPage); + + // When + when(airportService.getAllAirports(any(AirportPagingRequest.class))).thenReturn(taskPage); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/airports") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pagingRequest)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].id").value(expectedResponse.getContent().get(0).getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].name").value(expectedResponse.getContent().get(0).getName())); + + // Verify + verify(airportService, times(1)).getAllAirports(any(AirportPagingRequest.class)); + + } + + @Test + void givenAirportPagingRequest_whenGetAirportsFromUser_thenReturnCustomPageTask() throws Exception { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + final String airportId = UUID.randomUUID().toString(); + + final AirportEntity expectedEntity = new AirportEntityBuilder() + .withId(airportId) + .withValidFields(); + + final List airportEntities = List.of(expectedEntity); + + final Page airportEntityPage = new PageImpl<>(airportEntities, PageRequest.of(1, 1), airportEntities.size()); + + final List airportDomainModels = airportEntities.stream() + .map(entity -> new Airport(entity.getId(), entity.getName(), entity.getCityName())) + .collect(Collectors.toList()); + + final CustomPage taskPage = CustomPage.of(airportDomainModels, airportEntityPage); + + final CustomPagingResponse expectedResponse = + customPageAirportToCustomPagingAirportResponseMapper.toPagingResponse(taskPage); + + // When + when(airportService.getAllAirports(any(AirportPagingRequest.class))).thenReturn(taskPage); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/airports") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pagingRequest)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].id").value(expectedResponse.getContent().get(0).getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].name").value(expectedResponse.getContent().get(0).getName())); + + // Verify + verify(airportService, times(1)).getAllAirports(any(AirportPagingRequest.class)); + + } + + @Test + void givenAirportPagingRequest_WhenUnauthorized_thenThrowUnauthorized() throws Exception { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/airports") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pagingRequest))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(airportService, never()).getAllAirports(any(AirportPagingRequest.class)); + + } + + @Test + void givenValidAirportUpdate_WithAdminUpdate_whenUpdateAirport_thenSuccess() throws Exception{ + + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateAirportRequest request = new UpdateAirportRequestBuilder() + .withValidFields() + .build(); + + final Airport expectedAirport = new AirportBuilder() + .withId(mockId) + .withName(request.getName()) + .withCityName(request.getCityName()) + .build(); + + final AirportResponse expectedAirportResponse = airportToAirportResponseMapper.map(expectedAirport); + + // When + when(airportService.updateAirportById(anyString(),any(UpdateAirportRequest.class))) + .thenReturn(expectedAirport); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .put("/api/v1/airports/{id}",mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION,"Bearer " + mockAdminToken.getAccessToken()) + + ).andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.id").value(expectedAirportResponse.getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.name").value(expectedAirportResponse.getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.cityName").value(expectedAirportResponse.getCityName())); + + // Verify + verify(airportService,times(1)) + .updateAirportById(anyString(),any(UpdateAirportRequest.class)); + + } + + @Test + void givenAirportWithDuplicateName_whenUpdateAirport_thenThrowAirportWithThisNameAlreadyExistException() throws Exception { + + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateAirportRequest request = new UpdateAirportRequestBuilder() + .withName("Airport Duplicate Name") + .withCityName("City Name") + .build(); + + // When + when(airportService.updateAirportById(anyString(), any(UpdateAirportRequest.class))) + .thenThrow(new AirportNameAlreadyExistException("With given task name = " + request.getName())); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .put("/api/v1/airports/{id}", mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isBadRequest()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("BAD_REQUEST")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Airport with this name already exist\n With given task name = " + request.getName())); + + // Verify + verify(airportService, times(1)) + .updateAirportById(anyString(), any(UpdateAirportRequest.class)); + + } + + @Test + void givenInvalidAirportId_whenUpdateAirport_thenThrowNotFoundException() throws Exception { + + // Given + final String nonExistentTaskId = UUID.randomUUID().toString(); + + final UpdateAirportRequest request = new UpdateAirportRequestBuilder() + .withValidFields() + .build(); + + final String expectedMessage = "Airport not found!\n Airport not found with ID: " + nonExistentTaskId; + + // When + when(airportService.updateAirportById(anyString(), any(UpdateAirportRequest.class))) + .thenThrow(new AirportNotFoundException("Airport not found with ID: " + nonExistentTaskId)); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .put("/api/v1/airports/{id}", nonExistentTaskId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)); + + // Verify + verify(airportService, times(1)).updateAirportById(anyString(), any(UpdateAirportRequest.class)); + + } + + + @Test + void givenValidUpdateAirportRequest_whenUserUnAuthorized_thenReturnUnauthorized() throws Exception { + + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateAirportRequest request = new UpdateAirportRequestBuilder() + .withValidFields() + .build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.put("/api/v1/airports/{id}",mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION,"Bearer " + mockUserToken.getAccessToken()) + ).andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + + // Verify + verify(airportService,never()).updateAirportById(anyString(), any(UpdateAirportRequest.class)); + + } + + @Test + void givenValidUpdateRequest_whenUserNotAuthenticated_thenThrowUnAuthorize() throws Exception{ + + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateAirportRequest request = new UpdateAirportRequestBuilder() + .withValidFields() + .build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.put("/api/v1/tasks/{id}",mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(airportService,never()).updateAirportById(anyString(), any(UpdateAirportRequest.class)); + + } + + @Test + void givenValidAirportId_whenDeleteAirportByIdFromAdmin_thenSuccess() throws Exception { + + // Given + final String mockAirportId = UUID.randomUUID().toString(); + + // When + doNothing().when(airportService).deleteAirportById(mockAirportId); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/airports/{id}", mockAirportId) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response").value( + "Airport with id " + mockAirportId + "is deleted") + ); + + // Verify + verify(airportService, times(1)).deleteAirportById(mockAirportId); + + } + + @Test + void givenValidAirportId_whenDeleteAirportByIdFromUser_thenForbidden() throws Exception { + + // Given + final String mockAirportId = UUID.randomUUID().toString(); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/airports/{id}", mockAirportId) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + + // Verify + verify(airportService, never()).deleteAirportById(mockAirportId); + + } + + @Test + void givenValidAirportId_whenNotAuthenticated_thenUnauthorized() throws Exception { + + // Given + final String mockAirportId = UUID.randomUUID().toString(); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/airports/{id}", mockAirportId)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(airportService, never()).deleteAirportById(mockAirportId); + + } + + @Test + void givenInvalidAirportId_whenAdminDeletesAirport_thenAirportNotFoundException() throws Exception { + + // Given + final String mockAirportId = UUID.randomUUID().toString(); + final String expectedMessage = "Airport not found!\n"; + + // When + doThrow(new AirportNotFoundException()) + .when(airportService).deleteAirportById(mockAirportId); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/airports/{id}", mockAirportId) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)); + + // Verify + verify(airportService, times(1)).deleteAirportById(mockAirportId); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/FlightControllerTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/FlightControllerTest.java new file mode 100644 index 000000000..7d29fd071 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/FlightControllerTest.java @@ -0,0 +1,740 @@ +package com.example.demo.flight.controller; + +import com.example.demo.base.AbstractRestControllerTest; +import com.example.demo.builder.*; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.airport.AirportPagingRequest; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.CustomPageFlightToCustomPagingFlightResponseMapper; +import com.example.demo.flight.model.mapper.flight.FlightToFlightResponseMapper; +import com.example.demo.flight.service.flight.FlightService; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Integration test class for the {@link FlightController}. + * This class tests the REST endpoints of the {@link FlightController} to ensure that task-related + * operations are performed correctly. + */ +class FlightControllerTest extends AbstractRestControllerTest { + + @MockitoBean + FlightService flightService; + + private final FlightToFlightResponseMapper flightToFlightResponseMapper = FlightToFlightResponseMapper.initialize(); + + private final CustomPageFlightToCustomPagingFlightResponseMapper customPageFlightToCustomPagingFlightResponseMapper + = CustomPageFlightToCustomPagingFlightResponseMapper.initialize(); + + @Test + void givenValidCreateFlightRequestByAdmin_whenCreateFlight_thenSuccess() throws Exception { + + // Given + final LocalDateTime now = LocalDateTime.now(); + + final CreateFlightRequest request = new CreateFlightRequestBuilder() + .withValidFields() + .withDepartureTime(now.plusDays(1)) // Departure time is 1 day in the future + .withArrivalTime(now.plusDays(1).plusHours(2)) // Arrival time is 2 hours after departure + .build(); + + final Flight expectedFlight = Flight.builder() + .id(UUID.randomUUID().toString()) + .fromAirport(Airport.builder().id(request.getFromAirportId()).build()) + .toAirport(Airport.builder().id(request.getToAirportId()).build()) + .departureTime(request.getDepartureTime()) + .arrivalTime(request.getArrivalTime()) + .price(request.getPrice()) + .build(); + + // When + when(flightService.createFlight(any(CreateFlightRequest.class))).thenReturn(expectedFlight); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/v1/flights") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken()) + ).andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response").value(expectedFlight.getId())); + + // Verify + verify(flightService, times(1)).createFlight(any(CreateFlightRequest.class)); + + } + + @Test + void givenValidCreateFlightRequest_whenUnauthorized_thenThrowUnAuthorizedException() throws Exception { + + // Given + final LocalDateTime now = LocalDateTime.now(); + + final CreateFlightRequest request = new CreateFlightRequestBuilder() + .withValidFields() + .withDepartureTime(now.plusDays(1)) // Departure time is 1 day in the future + .withArrivalTime(now.plusDays(1).plusHours(2)) // Arrival time is 2 hours after departure + .build(); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/v1/flights") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(flightService, never()).createFlight(any(CreateFlightRequest.class)); + + } + + @Test + void givenValidCreateFlightRequest_whenForbiddenByUser_thenThrowForbidden() throws Exception { + + // Given + final LocalDateTime now = LocalDateTime.now(); + + final CreateFlightRequest request = new CreateFlightRequestBuilder() + .withValidFields() + .withDepartureTime(now.plusDays(1)) // Departure time is 1 day in the future + .withArrivalTime(now.plusDays(1).plusHours(2)) // Arrival time is 2 hours after departure + .build(); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/v1/flights") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken()) + .content(objectMapper.writeValueAsString(request)) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + + // Verify + verify(flightService, never()).createFlight(any(CreateFlightRequest.class)); + + } + + @Test + void givenInvalidArrivalTime_whenCreateFlight_thenThrowsValidationException() throws Exception { + + // Given + final LocalDateTime now = LocalDateTime.now(); + + final CreateFlightRequest invalidRequest = new CreateFlightRequestBuilder() + .withValidFields() + .withDepartureTime(now.plusDays(1)) // Departure time is 1 day in the future + .withArrivalTime(now.plusDays(1).minusHours(1)) // Invalid: Arrival time is 1 hour before departure + .build(); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/v1/flights") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isBadRequest()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("BAD_REQUEST")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Validation failed")) + .andExpect(MockMvcResultMatchers.jsonPath("$.subErrors[0].message").value("Arrival time must be the same as or later than departure time!")) + .andExpect(MockMvcResultMatchers.jsonPath("$.subErrors[0].field").value("createFlightRequest")); + + // Verify + verify(flightService, never()).createFlight(any(CreateFlightRequest.class)); + + } + + @Test + void givenExistFlightId_whenGetFlightByIdWithAdmin_thenReturnCustomResponse() throws Exception { + + // Given + final String mockFlightId = UUID.randomUUID().toString(); + + final Flight mockFlight = new FlightBuilder() + .withId(mockFlightId) + .withValidFields() + .build(); + + final FlightResponse expectedResponse = flightToFlightResponseMapper.map(mockFlight); + + // When + when(flightService.getFlightById(mockFlightId)).thenReturn(mockFlight); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/flights/{id}", mockFlightId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.id").value(expectedResponse.getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.id").value(expectedResponse.getFromAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.name").value(expectedResponse.getFromAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.cityName").value(expectedResponse.getFromAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.id").value(expectedResponse.getToAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.name").value(expectedResponse.getToAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.cityName").value(expectedResponse.getToAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.price").value(expectedResponse.getPrice())) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")); + + + // Verify + verify(flightService, times(1)).getFlightById(mockFlightId); + + } + + @Test + void givenExistFlightId_whenGetFlightByIdWithUser_thenReturnCustomResponse() throws Exception { + + // Given + final String mockFlightId = UUID.randomUUID().toString(); + + final Flight mockFlight = new FlightBuilder() + .withId(mockFlightId) + .withValidFields() + .build(); + + final FlightResponse expectedResponse = flightToFlightResponseMapper.map(mockFlight); + + // When + when(flightService.getFlightById(mockFlightId)).thenReturn(mockFlight); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/flights/{id}", mockFlightId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.id").value(expectedResponse.getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.id").value(expectedResponse.getFromAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.name").value(expectedResponse.getFromAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.cityName").value(expectedResponse.getFromAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.id").value(expectedResponse.getToAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.name").value(expectedResponse.getToAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.cityName").value(expectedResponse.getToAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.price").value(expectedResponse.getPrice())) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")); + + + // Verify + verify(flightService, times(1)).getFlightById(mockFlightId); + + } + + @Test + void givenNonExistFlightId_whenGetFlightByIdWithAdmin_thenThrowFlightNotFoundException() throws Exception { + + // Given + final String nonExistentFlightId = UUID.randomUUID().toString(); + final String expectedMessage = "Flight not found!\n Flight not found with ID: " + nonExistentFlightId; + + // When + when(flightService.getFlightById(nonExistentFlightId)) + .thenThrow(new FlightNotFoundException("Flight not found with ID: " + nonExistentFlightId)); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/flights/{id}", nonExistentFlightId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.header").value("API ERROR")) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)); + + // Verify + verify(flightService, times(1)).getFlightById(nonExistentFlightId); + + } + + @Test + void givenNonExistFlightId_whenGetFlightByIdWithUser_thenThrowFlightNotFoundException() throws Exception { + + // Given + final String nonExistentFlightId = UUID.randomUUID().toString(); + final String expectedMessage = "Flight not found!\n Flight not found with ID: " + nonExistentFlightId; + + // When + when(flightService.getFlightById(nonExistentFlightId)) + .thenThrow(new FlightNotFoundException("Flight not found with ID: " + nonExistentFlightId)); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/flights/{id}", nonExistentFlightId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.header").value("API ERROR")) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)); + + // Verify + verify(flightService, times(1)).getFlightById(nonExistentFlightId); + + } + + @Test + void givenExistFlightId_whenUserUnauthorized_thenReturnUnauthorized() throws Exception { + + // Given + final String mockFlightId = UUID.randomUUID().toString(); + + final Flight mockFlight = new FlightBuilder() + .withId(mockFlightId) + .withValidFields() + .build(); + + // When + when(flightService.getFlightById(mockFlightId)).thenReturn(mockFlight); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/flights/{id}", mockFlightId) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(flightService, never()).getFlightById(mockFlightId); + + } + + @Test + void givenFlightPagingRequest_whenGetFlightsFromAdmin_thenReturnCustomPageFlight() throws Exception { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + final String flightId = UUID.randomUUID().toString(); + + final FlightEntity expectedEntity = new FlightEntityBuilder() + .withId(flightId) + .withValidFields() + .build(); + + final List flightEntities = List.of(expectedEntity); + + final Page flightEntityPage = new PageImpl<>(flightEntities, PageRequest.of(1, 1), flightEntities.size()); + + final List flightDomainModels = flightEntities.stream() + .map(entity -> new Flight( + entity.getId(), + new Airport(entity.getFromAirport().getId(), entity.getFromAirport().getName(), entity.getFromAirport().getCityName()), + new Airport(entity.getToAirport().getId(), entity.getToAirport().getName(), entity.getToAirport().getCityName()), + entity.getDepartureTime(), + entity.getArrivalTime(), + entity.getPrice() + )) + .collect(Collectors.toList()); + + final CustomPage flightPage = CustomPage.of(flightDomainModels, flightEntityPage); + + final CustomPagingResponse expectedResponse = + customPageFlightToCustomPagingFlightResponseMapper.toPagingResponse(flightPage); + + // When + when(flightService.getAllFlights(any(AirportPagingRequest.class))).thenReturn(flightPage); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/flights") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pagingRequest)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].id").value(expectedResponse.getContent().get(0).getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.id").value(expectedResponse.getContent().get(0).getFromAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.name").value(expectedResponse.getContent().get(0).getFromAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.cityName").value(expectedResponse.getContent().get(0).getFromAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.id").value(expectedResponse.getContent().get(0).getToAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.name").value(expectedResponse.getContent().get(0).getToAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.cityName").value(expectedResponse.getContent().get(0).getToAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].price").value(expectedResponse.getContent().get(0).getPrice())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.pageNumber").value(expectedResponse.getPageNumber())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.pageSize").value(expectedResponse.getPageSize())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.totalElementCount").value(expectedResponse.getTotalElementCount())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.totalPageCount").value(expectedResponse.getTotalPageCount())); + + // Verify + verify(flightService, times(1)).getAllFlights(any(AirportPagingRequest.class)); + + } + + @Test + void givenFlightPagingRequest_whenGetFlightsFromUser_thenReturnCustomPageFlight() throws Exception { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + final String flightId = UUID.randomUUID().toString(); + + final FlightEntity expectedEntity = new FlightEntityBuilder() + .withId(flightId) + .withValidFields() + .build(); + + final List flightEntities = List.of(expectedEntity); + + final Page flightEntityPage = new PageImpl<>(flightEntities, PageRequest.of(1, 1), flightEntities.size()); + + final List flightDomainModels = flightEntities.stream() + .map(entity -> new Flight( + entity.getId(), + new Airport(entity.getFromAirport().getId(), entity.getFromAirport().getName(), entity.getFromAirport().getCityName()), + new Airport(entity.getToAirport().getId(), entity.getToAirport().getName(), entity.getToAirport().getCityName()), + entity.getDepartureTime(), + entity.getArrivalTime(), + entity.getPrice() + )) + .collect(Collectors.toList()); + + final CustomPage flightPage = CustomPage.of(flightDomainModels, flightEntityPage); + + final CustomPagingResponse expectedResponse = + customPageFlightToCustomPagingFlightResponseMapper.toPagingResponse(flightPage); + + // When + when(flightService.getAllFlights(any(AirportPagingRequest.class))).thenReturn(flightPage); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/flights") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pagingRequest)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].id").value(expectedResponse.getContent().get(0).getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.id").value(expectedResponse.getContent().get(0).getFromAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.name").value(expectedResponse.getContent().get(0).getFromAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.cityName").value(expectedResponse.getContent().get(0).getFromAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.id").value(expectedResponse.getContent().get(0).getToAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.name").value(expectedResponse.getContent().get(0).getToAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.cityName").value(expectedResponse.getContent().get(0).getToAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].price").value(expectedResponse.getContent().get(0).getPrice())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.pageNumber").value(expectedResponse.getPageNumber())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.pageSize").value(expectedResponse.getPageSize())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.totalElementCount").value(expectedResponse.getTotalElementCount())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.totalPageCount").value(expectedResponse.getTotalPageCount())); + + // Verify + verify(flightService, times(1)).getAllFlights(any(AirportPagingRequest.class)); + + } + + @Test + void givenFlightPagingRequest_whenUnauthorized_thenThrowUnauthorized() throws Exception { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/flights") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pagingRequest))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(flightService, never()).getAllFlights(any(AirportPagingRequest.class)); + + } + + @Test + void givenValidUpdateFlightRequest_whenTokenMissing_thenReturnUnauthorized() throws Exception { + + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateFlightRequest request = new UpdateFlightRequestBuilder() + .withValidFields() + .build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.put("/api/v1/flights/{id}", mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(flightService, never()).updateFlightById(anyString(), any(UpdateFlightRequest.class)); + + } + + @Test + void givenValidUpdateFlightRequest_whenAdminAuthorized_thenUpdateFlightAndReturnSuccess() throws Exception { + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateFlightRequest request = new UpdateFlightRequestBuilder() + .withValidFields() + .withDepartureTime(LocalDateTime.now().plusDays(1)) + .withArrivalTime(LocalDateTime.now().plusDays(2)) + .build(); + + final Flight mockFlight = new FlightBuilder() + .withId(mockId) + .withFromAirport(new AirportBuilder().withValidFields()) + .withToAirport(new AirportBuilder().withValidFields()) + .withDepartureTime(request.getDepartureTime()) + .withArrivalTime(request.getArrivalTime()) + .withPrice(request.getPrice()) + .build(); + + final FlightResponse response = flightToFlightResponseMapper.map(mockFlight); + + when(flightService.updateFlightById(eq(mockId), any(UpdateFlightRequest.class))).thenReturn(mockFlight); + + // Then + mockMvc.perform(MockMvcRequestBuilders.put("/api/v1/flights/{id}", mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.id").value(response.getId())) // Ensure the response ID matches the mocked one + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.id").value(response.getFromAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.name").value(response.getFromAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.fromAirport.cityName").value(response.getFromAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.id").value(response.getToAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.name").value(response.getToAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.toAirport.cityName").value(response.getToAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.price").value(response.getPrice())); + + // Verify + verify(flightService).updateFlightById(eq(mockId), any(UpdateFlightRequest.class)); + + } + + @Test + void givenInvalidFlightId_whenUpdateFlight_thenThrowNotFoundException() throws Exception { + + // Given + final String nonExistentFlightId = UUID.randomUUID().toString(); + final UpdateFlightRequest request = new UpdateFlightRequestBuilder() + .withValidFields() + .build(); + + final String expectedMessage = "Flight not found!\n Flight not found with ID: " + nonExistentFlightId; + + // When + when(flightService.updateFlightById(anyString(), any(UpdateFlightRequest.class))) + .thenThrow(new FlightNotFoundException("Flight not found with ID: " + nonExistentFlightId)); + + // Then + mockMvc.perform( + MockMvcRequestBuilders + .put("/api/v1/flights/{id}", nonExistentFlightId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)); + + // Verify + verify(flightService, times(1)).updateFlightById(anyString(), any(UpdateFlightRequest.class)); + + } + + @Test + void givenValidUpdateFlightRequest_whenUserUnAuthorized_thenReturnForbidden() throws Exception { + + // Given + final String mockId = UUID.randomUUID().toString(); + final UpdateFlightRequest request = new UpdateFlightRequestBuilder() + .withValidFields() + .build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.put("/api/v1/flights/{id}", mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken()) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + + // Verify + verify(flightService, never()).updateFlightById(anyString(), any(UpdateFlightRequest.class)); + + } + + @Test + void givenValidUpdateRequest_whenUserNotAuthenticated_thenThrowUnauthorized() throws Exception { + + // Given + final String mockId = UUID.randomUUID().toString(); + final UpdateFlightRequest request = new UpdateFlightRequestBuilder() + .withValidFields() + .build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.put("/api/v1/flights/{id}", mockId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(flightService, never()).updateFlightById(anyString(), any(UpdateFlightRequest.class)); + + } + + @Test + void givenValidFlightId_whenDeleteFlightByIdFromAdmin_thenSuccess() throws Exception { + + // Given + final String mockFlightId = UUID.randomUUID().toString(); + + // When + doNothing().when(flightService).deleteFlightById(mockFlightId); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/flights/{id}", mockFlightId) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response").value( + "Flight with id " + mockFlightId + "is deleted") + ); + + // Verify + verify(flightService, times(1)).deleteFlightById(mockFlightId); + + } + + @Test + void givenValidAirportId_whenDeleteAirportByIdFromUser_thenForbidden() throws Exception { + + // Given + final String mockFlightId = UUID.randomUUID().toString(); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/flights/{id}", mockFlightId) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + + // Verify + verify(flightService, never()).deleteFlightById(mockFlightId); + + } + + @Test + void givenValidAirportId_whenNotAuthenticated_thenUnauthorized() throws Exception { + + // Given + final String mockFlightId = UUID.randomUUID().toString(); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/flights/{id}", mockFlightId)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(flightService, never()).deleteFlightById(mockFlightId); + + } + + @Test + void givenInvalidFlightId_whenAdminDeletesFlight_thenFlightNotFoundException() throws Exception { + + // Given + final String mockFlightId = UUID.randomUUID().toString(); + final String expectedMessage = "Flight not found!\n"; + + // When + doThrow(new FlightNotFoundException()) + .when(flightService).deleteFlightById(mockFlightId); + + // Then + mockMvc.perform(MockMvcRequestBuilders + .delete("/api/v1/flights/{id}", mockFlightId) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("NOT_FOUND")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(false)) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value(expectedMessage)); + + // Verify + verify(flightService, times(1)).deleteFlightById(mockFlightId); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/SearchFlightControllerTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/SearchFlightControllerTest.java new file mode 100644 index 000000000..e7bed6cfd --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/controller/SearchFlightControllerTest.java @@ -0,0 +1,239 @@ +package com.example.demo.flight.controller; + +import com.example.demo.base.AbstractRestControllerTest; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.builder.FlightEntityBuilder; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.SearchFlightRequest; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.CustomPageFlightToCustomPagingFlightResponseMapper; +import com.example.demo.flight.service.flight.SearchFlightService; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.testcontainers.shaded.com.google.common.net.HttpHeaders; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link SearchFlightController}. + * This class tests the REST API endpoints of the {@link SearchFlightController}, ensuring that the + * search flight functionality works as expected. It validates the integration between the controller, + * the {@link SearchFlightService}, and the mapping logic provided by the + * {@link CustomPageFlightToCustomPagingFlightResponseMapper}. + */ +class SearchFlightControllerTest extends AbstractRestControllerTest { + + @MockitoBean + SearchFlightService searchFlightService; + + private final CustomPageFlightToCustomPagingFlightResponseMapper customPageFlightToCustomPagingFlightResponseMapper = + CustomPageFlightToCustomPagingFlightResponseMapper.initialize(); + + @Test + void givenSearchFlightRequest_whenOneWayFlightsFoundFromAdmin_thenReturnFlights() throws Exception { + + // Given + final CustomPaging pagination = CustomPaging.builder() + .pageNumber(1) + .pageSize(10) + .build(); + + final SearchFlightRequest request = SearchFlightRequest.builder() + .fromAirportId(UUID.randomUUID().toString()) + .toAirportId(UUID.randomUUID().toString()) + .departureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .pagination(pagination) + .build(); + + final FlightEntity flightEntity = new FlightEntityBuilder() + .withValidFields() + .withFromAirport(new AirportEntityBuilder() + .withId(request.getFromAirportId()) + .withValidFields()) + .withToAirport(new AirportEntityBuilder() + .withId(request.getToAirportId()) + .withValidFields()) + .withDepartureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .build(); + + final List flightEntities = List.of(flightEntity); + + final Page flightEntityPage = new PageImpl<>(flightEntities, PageRequest.of(0, 1), flightEntities.size()); + + final List flightDomainModels = flightEntities.stream() + .map(entity -> Flight.builder() + .id(entity.getId()) + .fromAirport(Airport.builder() + .id(entity.getFromAirport().getId()) + .name(entity.getFromAirport().getName()) + .cityName(entity.getFromAirport().getCityName()) + .build()) + .toAirport(Airport.builder() + .id(entity.getToAirport().getId()) + .name(entity.getToAirport().getName()) + .cityName(entity.getToAirport().getCityName()) + .build()) + .departureTime(entity.getDepartureTime()) + .arrivalTime(entity.getArrivalTime()) + .price(entity.getPrice()) + .build()) + .collect(Collectors.toList()); + + + final CustomPage flightPage = CustomPage.of(flightDomainModels, flightEntityPage); + + final CustomPagingResponse expectedResponse = + customPageFlightToCustomPagingFlightResponseMapper.toPagingResponse(flightPage); + + // When + when(searchFlightService.searchFlights(any(SearchFlightRequest.class))).thenReturn(flightPage); + + // Then + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/flights/search") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockAdminToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.id").value(expectedResponse.getContent().get(0).getFromAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.name").value(expectedResponse.getContent().get(0).getFromAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.cityName").value(expectedResponse.getContent().get(0).getFromAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.id").value(expectedResponse.getContent().get(0).getToAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.name").value(expectedResponse.getContent().get(0).getToAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.cityName").value(expectedResponse.getContent().get(0).getToAirport().getCityName())); + + // Verify + verify(searchFlightService, times(1)).searchFlights(any(SearchFlightRequest.class)); + + } + + @Test + void givenSearchFlightRequest_whenOneWayFlightsFoundFromUser_thenReturnFlights() throws Exception { + + // Given + final CustomPaging pagination = CustomPaging.builder() + .pageNumber(1) + .pageSize(10) + .build(); + + final SearchFlightRequest request = SearchFlightRequest.builder() + .fromAirportId(UUID.randomUUID().toString()) + .toAirportId(UUID.randomUUID().toString()) + .departureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .pagination(pagination) + .build(); + + final FlightEntity flightEntity = new FlightEntityBuilder() + .withValidFields() + .withFromAirport(new AirportEntityBuilder() + .withId(request.getFromAirportId()) + .withValidFields()) + .withToAirport(new AirportEntityBuilder() + .withId(request.getToAirportId()) + .withValidFields()) + .withDepartureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .build(); + + final List flightEntities = List.of(flightEntity); + + final Page flightEntityPage = new PageImpl<>(flightEntities, PageRequest.of(0, 1), flightEntities.size()); + + final List flightDomainModels = flightEntities.stream() + .map(entity -> Flight.builder() + .id(entity.getId()) + .fromAirport(Airport.builder() + .id(entity.getFromAirport().getId()) + .name(entity.getFromAirport().getName()) + .cityName(entity.getFromAirport().getCityName()) + .build()) + .toAirport(Airport.builder() + .id(entity.getToAirport().getId()) + .name(entity.getToAirport().getName()) + .cityName(entity.getToAirport().getCityName()) + .build()) + .departureTime(entity.getDepartureTime()) + .arrivalTime(entity.getArrivalTime()) + .price(entity.getPrice()) + .build()) + .collect(Collectors.toList()); + + + final CustomPage flightPage = CustomPage.of(flightDomainModels, flightEntityPage); + + final CustomPagingResponse expectedResponse = + customPageFlightToCustomPagingFlightResponseMapper.toPagingResponse(flightPage); + + // When + when(searchFlightService.searchFlights(any(SearchFlightRequest.class))).thenReturn(flightPage); + + // Then + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/flights/search") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + mockUserToken.getAccessToken())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpStatus").value("OK")) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.id").value(expectedResponse.getContent().get(0).getFromAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.name").value(expectedResponse.getContent().get(0).getFromAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].fromAirport.cityName").value(expectedResponse.getContent().get(0).getFromAirport().getCityName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.id").value(expectedResponse.getContent().get(0).getToAirport().getId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.name").value(expectedResponse.getContent().get(0).getToAirport().getName())) + .andExpect(MockMvcResultMatchers.jsonPath("$.response.content[0].toAirport.cityName").value(expectedResponse.getContent().get(0).getToAirport().getCityName())); + + // Verify + verify(searchFlightService, times(1)).searchFlights(any(SearchFlightRequest.class)); + + } + + @Test + void givenSearchFlightRequest_whenUnauthorized_thenThrowUnauthorized() throws Exception { + + // Given + final CustomPaging pagination = CustomPaging.builder() + .pageNumber(1) + .pageSize(10) + .build(); + + final SearchFlightRequest searchFlightRequest = SearchFlightRequest.builder() + .fromAirportId(UUID.randomUUID().toString()) + .toAirportId(UUID.randomUUID().toString()) + .departureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .pagination(pagination) + .build(); + + // Then + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/flights/search") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(searchFlightRequest))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + // Verify + verify(searchFlightService, never()).searchFlights(any(SearchFlightRequest.class)); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/exception/AirportNameAlreadyExistExceptionTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/exception/AirportNameAlreadyExistExceptionTest.java new file mode 100644 index 000000000..5ee30b87a --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/exception/AirportNameAlreadyExistExceptionTest.java @@ -0,0 +1,22 @@ +package com.example.demo.flight.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for verifying the behavior of the {@link AirportNameAlreadyExistException}. + * It checks the default constructor to ensure the correct error message is set. + */ +class AirportNameAlreadyExistExceptionTest { + + @Test + void testConstructor_DefaultMessage() { + + AirportNameAlreadyExistException exception = new AirportNameAlreadyExistException(); + + assertEquals("Airport with this name already exist\n", exception.getMessage()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/exception/FlightNotFoundExceptionTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/exception/FlightNotFoundExceptionTest.java new file mode 100644 index 000000000..74fe13505 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/exception/FlightNotFoundExceptionTest.java @@ -0,0 +1,27 @@ +package com.example.demo.flight.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Test class for verifying the behavior of the {@link FlightNotFoundException}. + * This class contains unit tests to validate that the exception behaves as expected + * when constructed with default and custom messages. + */ +class FlightNotFoundExceptionTest { + + @Test + void testDefaultConstructor() { + + // Act + FlightNotFoundException exception = new FlightNotFoundException(); + + // Assert + assertNotNull(exception); + assertEquals("Flight not found!\n", exception.getMessage()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/job/FlightDataLoaderTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/job/FlightDataLoaderTest.java new file mode 100644 index 000000000..de1a1d8d3 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/job/FlightDataLoaderTest.java @@ -0,0 +1,69 @@ +package com.example.demo.flight.job; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.repository.FlightRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.Mockito.*; + +/** + * Unit test for {@link FlightDataLoader}. + * This test verifies that the dummy flight data is properly loaded + * and that interactions with the {@link FlightRepository} and {@link AirportRepository} + * occur as expected. + */ +class FlightDataLoaderTest extends AbstractBaseServiceTest { + + @InjectMocks + private FlightDataLoader flightDataLoader; + + @Mock + private FlightRepository flightRepository; + + @Mock + private AirportRepository airportRepository; + + @Test + void shouldLoadFlightDummyData() { + + // Given + AirportEntity airport1 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Los Angeles International Airport") + .cityName("Los Angeles") + .build(); + + AirportEntity airport2 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("John F. Kennedy International Airport") + .cityName("New York") + .build(); + + AirportEntity airport3 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Chicago O'Hare International Airport") + .cityName("Chicago") + .build(); + + List mockAirports = List.of(airport1, airport2, airport3); + + // When + when(airportRepository.findAll()).thenReturn(mockAirports); + + // Then + flightDataLoader.loadFlightDumpyData(); + + // Verify + verify(airportRepository, times(1)).saveAll(anyList()); + verify(flightRepository, times(1)).saveAll(anyList()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/job/ScheduledJobTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/job/ScheduledJobTest.java new file mode 100644 index 000000000..e3d3a77ff --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/job/ScheduledJobTest.java @@ -0,0 +1,44 @@ +package com.example.demo.flight.job; + +import com.example.demo.base.AbstractBaseServiceTest; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.*; + +/** + * Unit test for {@link ScheduledJob}. + * Verifies that {@link ScheduledJob#loadMockFlightsToDatabase()} triggers + * {@link FlightDataLoader#loadFlightDumpyData()} once. + * Uses Awaitility to wait up to 5 seconds for the method call. + */ +class ScheduledJobTest extends AbstractBaseServiceTest { + + @InjectMocks + private ScheduledJob scheduledJob; + + @Mock + private FlightDataLoader flightDataLoader; + + @Test + void testLoadMockFlightsToDatabase() { + // Arrange: Do nothing when flight data loader is called + doNothing().when(flightDataLoader).loadFlightDumpyData(); + + // Act: Manually trigger the scheduled job + scheduledJob.loadMockFlightsToDatabase(); + + // Assert: Wait and verify that the method was called + // Waits up to 5 seconds, checking condition periodically + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> + verify(flightDataLoader, times(1)).loadFlightDumpyData() + ); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/AirportEntityToAirportMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/AirportEntityToAirportMapperTest.java new file mode 100644 index 000000000..1c58171a8 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/AirportEntityToAirportMapperTest.java @@ -0,0 +1,138 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.entity.AirportEntity; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link AirportEntityToAirportMapper}. + * This class ensures that {@link AirportEntity} objects are correctly mapped to their domain-level representation. + */ +class AirportEntityToAirportMapperTest { + + private final AirportEntityToAirportMapper mapper = AirportEntityToAirportMapper.initialize(); + + @Test + void testMapAirportEntityNull() { + // Test null single TaskEntity + Airport result = mapper.map((AirportEntity) null); + assertNull(result); + } + + @Test + void testMapAirportEntityCollectionNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapAirportEntityListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapAirportEntityListWithNullElements() { + + List airportEntities = Arrays.asList(new AirportEntity(), null); + List result = mapper.map(airportEntities); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNull(result.get(1)); + + } + + @Test + void testMapSingleAirportEntity() { + + AirportEntity airportEntity = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Airport Name") + .cityName("Airport City Name") + .createdAt(LocalDateTime.now()) + .createdBy("user1") + .updatedAt(LocalDateTime.now()) + .updatedBy("user2") + .build(); + + Airport result = mapper.map(airportEntity); + + assertNotNull(result); + assertEquals(airportEntity.getId(), result.getId()); + assertEquals(airportEntity.getName(), result.getName()); + assertEquals(airportEntity.getCreatedAt(), result.getCreatedAt()); + assertEquals(airportEntity.getCreatedBy(), result.getCreatedBy()); + assertEquals(airportEntity.getUpdatedAt(), result.getUpdatedAt()); + assertEquals(airportEntity.getUpdatedBy(), result.getUpdatedBy()); + + } + + @Test + void testMapAirportEntityListWithValues() { + + AirportEntity airportEntity1 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Airport Name 1") + .cityName("Airport City Name 1") + .createdAt(LocalDateTime.now()) + .createdBy("user1") + .updatedAt(LocalDateTime.now()) + .updatedBy("user2") + .build(); + + AirportEntity airportEntity2 = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Airport Name 2") + .cityName("Airport City Name 2") + .createdAt(LocalDateTime.now()) + .createdBy("user1") + .updatedAt(LocalDateTime.now()) + .updatedBy("user2") + .build(); + + List taskEntities = Arrays.asList(airportEntity1, airportEntity2); + List result = mapper.map(taskEntities); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(airportEntity1.getId(), result.get(0).getId()); + assertEquals(airportEntity2.getId(), result.get(1).getId()); + + } + + @Test + void testMapWithEdgeCaseValues() { + + AirportEntity airportEntity = AirportEntity.builder() + .name("") + .createdAt(null) + .createdBy(null) + .updatedAt(null) + .updatedBy(null) + .build(); + + Airport result = mapper.map(airportEntity); + + assertNotNull(result); + assertEquals("", result.getName()); + assertNull(result.getCreatedAt()); + assertNull(result.getCreatedBy()); + assertNull(result.getUpdatedAt()); + assertNull(result.getUpdatedBy()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/AirportToAirportResponseMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/AirportToAirportResponseMapperTest.java new file mode 100644 index 000000000..f6c464efc --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/AirportToAirportResponseMapperTest.java @@ -0,0 +1,131 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.response.airport.AirportResponse; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link AirportToAirportResponseMapperTest}. + * This class verifies the correctness of the mapping logic from Airport objects to {@link AirportResponse}. + */ +class AirportToAirportResponseMapperTest { + + private final AirportToAirportResponseMapper mapper = AirportToAirportResponseMapper.initialize(); + + @Test + void testMapAirportNull() { + + AirportResponse result = mapper.map((Airport) null); + + assertNull(result); + + } + + @Test + void testMapAirportCollectionNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapAirportListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapAirportListWithNullElements() { + + List tasks = Arrays.asList(new Airport(), null); + + List result = mapper.map(tasks); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNull(result.get(1)); + + } + + @Test + void testMapSingleAirport() { + + Airport airport = Airport.builder() + .id(UUID.randomUUID().toString()) + .name("Airport Name") + .cityName("Airport City Name") + .createdAt(LocalDateTime.now()) + .createdBy("user1") + .updatedAt(LocalDateTime.now()) + .updatedBy("user2") + .build(); + + AirportResponse result = mapper.map(airport); + + assertNotNull(result); + assertEquals(airport.getId(), result.getId()); + assertEquals(airport.getName(), result.getName()); + + } + + @Test + void testMapAirportListWithValues() { + + Airport airport1 = Airport.builder() + .id(UUID.randomUUID().toString()) + .name("Airport Name 1") + .cityName("Airport City Name 1") + .createdAt(LocalDateTime.now()) + .createdBy("user1") + .updatedAt(LocalDateTime.now()) + .updatedBy("user2") + .build(); + + Airport airport2 = Airport.builder() + .id(UUID.randomUUID().toString()) + .name("Airport Name 2") + .cityName("Airport City Name 2") + .createdAt(LocalDateTime.now()) + .createdBy("user1") + .updatedAt(LocalDateTime.now()) + .updatedBy("user2") + .build(); + + List tasks = Arrays.asList(airport1, airport2); + List result = mapper.map(tasks); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(airport1.getId(), result.get(0).getId()); + assertEquals(airport2.getId(), result.get(1).getId()); + + } + + @Test + void testMapWithEdgeCaseValues() { + + Airport airport = Airport.builder() + .id("") + .name("") + .build(); + + AirportResponse result = mapper.map(airport); + + assertNotNull(result); + assertEquals("", result.getId()); + assertEquals("", result.getName()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/CreateAirportRequestToAirportEntityMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/CreateAirportRequestToAirportEntityMapperTest.java new file mode 100644 index 000000000..f176e6ea1 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/CreateAirportRequestToAirportEntityMapperTest.java @@ -0,0 +1,118 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link CreateAirportRequestToAirportEntityMapperTest}. + * This class verifies the correctness of the mapping logic from {@link CreateAirportRequest} + * to {@link AirportEntity}. + */ +class CreateAirportRequestToAirportEntityMapperTest { + + private final CreateAirportRequestToAirportEntityMapper mapper = CreateAirportRequestToAirportEntityMapper.initialize(); + + @Test + void testMapCreateAirportRequestNull() { + + AirportEntity result = mapper.map((CreateAirportRequest) null); + + assertNull(result); + + } + + @Test + void testMapCreateAirportRequestCollectionNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapCreateAirportRequestListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapCreateAirportRequestListWithNullElements() { + + List requests = + Arrays.asList(new CreateAirportRequest("airportName","airportCityName"),null); + + List result = mapper.map(requests); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNull(result.get(1)); + + } + + @Test + void testMapSingleCreateAirportRequest() { + + CreateAirportRequest request = new CreateAirportRequest("airportName","airportCityName"); + + AirportEntity result = mapper.map(request); + + assertNotNull(result); + assertEquals(request.getName(), result.getName()); + + } + + @Test + void testMapCreateAirportRequestListWithValues() { + + CreateAirportRequest request1 = new CreateAirportRequest("airportName1","airportCityName1"); + CreateAirportRequest request2 = new CreateAirportRequest("airportName2","airportCityName2"); + + List requests = Arrays.asList(request1, request2); + + List result = mapper.map(requests); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(request1.getName(), result.get(0).getName()); + assertEquals(request2.getName(), result.get(1).getName()); + + } + + @Test + void testMapForSaving() { + + CreateAirportRequest request = new CreateAirportRequest("airportName","airportCityName"); + + AirportEntity result = mapper.mapForSaving(request); + + assertNotNull(result); + assertEquals(request.getName(), result.getName()); + + } + + @Test + void testMapWithEdgeCaseValues() { + + CreateAirportRequest request = new CreateAirportRequest("",""); + + AirportEntity result = mapper.map(request); + + assertNotNull(result); + assertEquals("", result.getName()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/CustomPageAirportToCustomPagingAirportResponseMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/CustomPageAirportToCustomPagingAirportResponseMapperTest.java new file mode 100644 index 000000000..d3faa7cb9 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/CustomPageAirportToCustomPagingAirportResponseMapperTest.java @@ -0,0 +1,48 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.response.airport.AirportResponse; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Test class for verifying the behavior of the {@link CustomPageAirportToCustomPagingAirportResponseMapper}. + * It tests the mapping methods, ensuring that they return {@code null} when the input is {@code null}. + */ +class CustomPageAirportToCustomPagingAirportResponseMapperTest { + + private final CustomPageAirportToCustomPagingAirportResponseMapper mapper = + CustomPageAirportToCustomPagingAirportResponseMapper.initialize(); + + @Test + void testToPagingResponse_WhenAirportPageIsNull() { + + // Test case where airportPage is null + CustomPage airportPage = null; + + CustomPagingResponse result = mapper.toPagingResponse(airportPage); + + // Assert that the result is null when airportPage is null + assertNull(result); + + } + + @Test + void testToAirportResponseList_WhenAirportsIsNull() { + + // Test case where airports is null + List airports = null; + + List result = mapper.toAirportResponseList(airports); + + // Assert that the result is null when airports is null + assertNull(result); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/ListAirportEntityToListAirportMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/ListAirportEntityToListAirportMapperTest.java new file mode 100644 index 000000000..954909724 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/ListAirportEntityToListAirportMapperTest.java @@ -0,0 +1,32 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.entity.AirportEntity; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Test class for verifying the behavior of the {@link ListAirportEntityToListAirportMapper}. + * It tests the {@link ListAirportEntityToListAirportMapper#toAirportList} method to ensure that it + * returns {@code null} when the input list of {@link AirportEntity} objects is {@code null}. + */ +class ListAirportEntityToListAirportMapperTest { + + private final ListAirportEntityToListAirportMapper mapper = + ListAirportEntityToListAirportMapper.initialize(); + + @Test + void testToAirportList_WhenAirportEntitiesIsNull() { + // Test case where airportEntities is null + List airportEntities = null; + + List result = mapper.toAirportList(airportEntities); + + // Assert that the result is null when airportEntities is null + assertNull(result); + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/UpdateAirportRequestToAirportEntityMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/UpdateAirportRequestToAirportEntityMapperTest.java new file mode 100644 index 000000000..511d9bc01 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/airport/UpdateAirportRequestToAirportEntityMapperTest.java @@ -0,0 +1,127 @@ +package com.example.demo.flight.model.mapper.airport; + +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link UpdateAirportRequestToAirportEntityMapper}. + * This class tests the functionality of mapping between {@link UpdateAirportRequest} + * and {@link AirportEntity} using the {@link UpdateAirportRequestToAirportEntityMapper}. + */ +class UpdateAirportRequestToAirportEntityMapperTest { + + private final UpdateAirportRequestToAirportEntityMapper mapper = UpdateAirportRequestToAirportEntityMapper.initialize(); + + @Test + void testMap_WhenUpdateAirportRequestIsNull() { + + AirportEntity result = mapper.map((UpdateAirportRequest) null); + + assertNull(result); + + } + + @Test + void testMap_WhenUpdateAirportRequestIsValid() { + + UpdateAirportRequest updateAirportRequest = new UpdateAirportRequest("Airport Name", + "City Name"); + + AirportEntity result = mapper.map(updateAirportRequest); + + assertNotNull(result); + assertEquals("Airport Name", result.getName()); + assertEquals("City Name", result.getCityName()); + + } + + @Test + void testMap_WhenUpdateAirportRequestIsEmpty() { + + UpdateAirportRequest updateAirportRequest = new UpdateAirportRequest("", ""); + + AirportEntity result = mapper.map(updateAirportRequest); + + assertNotNull(result); + assertEquals("", result.getName()); + assertEquals("", result.getCityName()); + + } + + @Test + void testMap_WhenUpdateAirportRequestHasNullNameAndCity() { + + UpdateAirportRequest updateAirportRequest = new UpdateAirportRequest(null, null); + + AirportEntity result = mapper.map(updateAirportRequest); + + assertNotNull(result); + assertNull(result.getName()); + assertNull(result.getCityName()); + + } + + @Test + void testMap_WhenUpdateAirportRequestCollectionIsNull() { + + List updateAirportRequests = null; + + List result = mapper.map(updateAirportRequests); + + assertNull(result); + + } + + @Test + void testMap_WhenUpdateAirportRequestCollectionIsEmpty() { + + List updateAirportRequests = Collections.emptyList(); + + List result = mapper.map(updateAirportRequests); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMap_WhenUpdateAirportRequestCollectionHasValidElements() { + + UpdateAirportRequest updateAirportRequest1 = new UpdateAirportRequest("Airport 1", "City 1"); + UpdateAirportRequest updateAirportRequest2 = new UpdateAirportRequest("Airport 2", "City 2"); + + List updateAirportRequests = Arrays.asList(updateAirportRequest1, updateAirportRequest2); + + List result = mapper.map(updateAirportRequests); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals("Airport 1", result.get(0).getName()); + assertEquals("City 1", result.get(0).getCityName()); + assertEquals("Airport 2", result.get(1).getName()); + assertEquals("City 2", result.get(1).getCityName()); + + } + + @Test + void testUpdateAirportMapper_WhenAirportEntityIsNull() { + + AirportEntity airportEntity = new AirportEntity(); + UpdateAirportRequest updateAirportRequest = new UpdateAirportRequest("Updated Airport", + "Updated City"); + + mapper.updateAirportMapper(airportEntity, updateAirportRequest); + + assertEquals("Updated Airport", airportEntity.getName()); + assertEquals("Updated City", airportEntity.getCityName()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/CreateFlightRequestToFlightEntityMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/CreateFlightRequestToFlightEntityMapperTest.java new file mode 100644 index 000000000..690a30766 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/CreateFlightRequestToFlightEntityMapperTest.java @@ -0,0 +1,173 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.entity.FlightEntity; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link CreateFlightRequestToFlightEntityMapper}. + * This class tests the functionality of the {@link CreateFlightRequestToFlightEntityMapper}, + * ensuring that {@link CreateFlightRequest} objects are correctly mapped to {@link FlightEntity} objects. + * The tests verify that all fields are mapped properly and that no data is lost or incorrectly transformed during the mapping process. + */ +class CreateFlightRequestToFlightEntityMapperTest { + + private final CreateFlightRequestToFlightEntityMapperImpl mapper = new CreateFlightRequestToFlightEntityMapperImpl(); + + @Test + void testMapCreateFlightRequestNull() { + + FlightEntity result = mapper.map((CreateFlightRequest) null); + + assertNull(result); + + } + + @Test + void testMapCreateFlightRequestListNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapCreateFlightRequestListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapCreateFlightRequestListWithNullElements() { + + CreateFlightRequest request1 = CreateFlightRequest.builder() + .departureTime(LocalDateTime.now()) + .arrivalTime(LocalDateTime.now().plusHours(2)) + .price(200.0) + .build(); + CreateFlightRequest request2 = null; + + List requests = Arrays.asList(request1, request2); + + List result = mapper.map(requests); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNotNull(result.get(0)); + assertNull(result.get(1)); + + } + + @Test + void testMapCreateFlightRequestForSaving() { + + CreateFlightRequest request = CreateFlightRequest.builder() + .departureTime(LocalDateTime.now()) + .arrivalTime(LocalDateTime.now().plusHours(2)) + .price(200.0) + .build(); + + FlightEntity result = mapper.map(request); + + assertNotNull(result); + assertEquals(request.getDepartureTime(), result.getDepartureTime()); + assertEquals(request.getArrivalTime(), result.getArrivalTime()); + assertEquals(request.getPrice(), result.getPrice()); + + } + + @Test + void testMapCreateFlightRequestListWithValues() { + + CreateFlightRequest request1 = CreateFlightRequest.builder() + .departureTime(LocalDateTime.now()) + .arrivalTime(LocalDateTime.now().plusHours(2)) + .price(200.0) + .build(); + + CreateFlightRequest request2 = CreateFlightRequest.builder() + .departureTime(LocalDateTime.now()) + .arrivalTime(LocalDateTime.now().plusHours(3)) + .price(300.0) + .build(); + + List requests = Arrays.asList(request1, request2); + + List result = mapper.map(requests); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(request1.getPrice(), result.get(0).getPrice()); + assertEquals(request2.getPrice(), result.get(1).getPrice()); + + } + + @Test + void testMapCreateFlightRequestForSavingWithNullValues() { + + CreateFlightRequest request = CreateFlightRequest.builder() + .departureTime(null) + .arrivalTime(null) + .price(null) + .build(); + + FlightEntity result = mapper.map(request); + + assertNotNull(result); + assertNull(result.getDepartureTime()); + assertNull(result.getArrivalTime()); + assertNull(result.getPrice()); + + } + + @Test + void testMapCreateFlightRequestListWithEdgeCaseValues() { + + CreateFlightRequest request = CreateFlightRequest.builder() + .departureTime(null) + .arrivalTime(null) + .price(0.0) + .build(); + + FlightEntity result = mapper.map(request); + + assertNotNull(result); + assertNull(result.getDepartureTime()); + assertNull(result.getArrivalTime()); + assertEquals(0.0, result.getPrice()); + + } + + @Test + void testMapCreateFlightRequestForSavingWithNullFields() { + + CreateFlightRequest request = CreateFlightRequest.builder() + .departureTime(null) + .arrivalTime(null) + .price(0.0) + .build(); + + FlightEntity result = mapper.map(request); + + assertNotNull(result); + assertNull(result.getDepartureTime()); + assertNull(result.getArrivalTime()); + assertEquals(0.0, result.getPrice()); + + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/CustomPageFlightToCustomPagingFlightResponseMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/CustomPageFlightToCustomPagingFlightResponseMapperTest.java new file mode 100644 index 000000000..a3b6fbb71 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/CustomPageFlightToCustomPagingFlightResponseMapperTest.java @@ -0,0 +1,60 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.dto.response.CustomPagingResponse; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link CustomPageFlightToCustomPagingFlightResponseMapper}. + * This class tests the functionality of the {@link CustomPageFlightToCustomPagingFlightResponseMapper}, + * ensuring that paginated flight data is correctly mapped to a custom paginated response format. + * The tests verify that all fields, including pagination metadata and flight details, are accurately + * transformed from the source to the target representation. + */ +class CustomPageFlightToCustomPagingFlightResponseMapperTest { + + private final CustomPageFlightToCustomPagingFlightResponseMapper mapper = + CustomPageFlightToCustomPagingFlightResponseMapper.initialize(); + + @Test + void testToPagingResponse_WhenFlightPageIsNull() { + + CustomPage flightPage = null; + + CustomPagingResponse result = mapper.toPagingResponse(flightPage); + + assertNull(result); + + } + + @Test + void testToFlightResponseList_WhenFlightsIsNull() { + + List flights = null; + + List result = mapper.toFlightResponseList(flights); + + assertNull(result); + } + + @Test + void testToFlightResponseList_WhenFlightsIsEmpty() { + + List flights = Collections.emptyList(); + + List result = mapper.toFlightResponseList(flights); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/FlightEntityToFlightMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/FlightEntityToFlightMapperTest.java new file mode 100644 index 000000000..8122be5e6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/FlightEntityToFlightMapperTest.java @@ -0,0 +1,149 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link FlightEntityToFlightMapper}. + * This class ensures that {@link FlightEntity} objects are correctly mapped to their domain-level representation. + */ +class FlightEntityToFlightMapperTest { + + private final FlightEntityToFlightMapper mapper = FlightEntityToFlightMapper.initialize(); + + @Test + void testMapFlightEntityNull() { + + // Test null single FlightEntity + Flight result = mapper.map((FlightEntity) null); + assertNull(result); + + } + + @Test + void testMapFlightEntityCollectionNull() { + + // Test null collection of FlightEntity + List result = mapper.map((Collection) null); + assertNull(result); + + } + + @Test + void testMapFlightEntityListEmpty() { + + // Test empty list of FlightEntity + List result = mapper.map(Collections.emptyList()); + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapFlightEntityListWithNullElements() { + + // Test list with null FlightEntity elements + FlightEntity flightEntity1 = new FlightEntity(); + List flightEntities = Arrays.asList(flightEntity1, null); + List result = mapper.map(flightEntities); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNull(result.get(1)); + + } + + @Test + void testMapSingleFlightEntity() { + + // Test mapping a single FlightEntity to Flight + FlightEntity flightEntity = FlightEntity.builder() + .id(UUID.randomUUID().toString()) + .fromAirport(AirportEntity.builder().name("From Airport").cityName("City A").build()) + .toAirport(AirportEntity.builder().name("To Airport").cityName("City B").build()) + .departureTime(LocalDateTime.now().plusHours(2)) + .arrivalTime(LocalDateTime.now().plusHours(3)) + .price(150.00) + .build(); + + Flight result = mapper.map(flightEntity); + + assertNotNull(result); + assertEquals(flightEntity.getId(), result.getId()); + assertNotNull(result.getFromAirport()); + assertNotNull(result.getToAirport()); + assertEquals(flightEntity.getFromAirport().getName(), result.getFromAirport().getName()); + assertEquals(flightEntity.getToAirport().getName(), result.getToAirport().getName()); + assertEquals(flightEntity.getDepartureTime(), result.getDepartureTime()); + assertEquals(flightEntity.getArrivalTime(), result.getArrivalTime()); + assertEquals(flightEntity.getPrice(), result.getPrice()); + + } + + @Test + void testMapFlightEntityListWithValues() { + + // Test mapping a list of FlightEntity to Flight + FlightEntity flightEntity1 = FlightEntity.builder() + .id(UUID.randomUUID().toString()) + .fromAirport(AirportEntity.builder().name("From Airport 1").cityName("City A1").build()) + .toAirport(AirportEntity.builder().name("To Airport 1").cityName("City B1").build()) + .departureTime(LocalDateTime.now().plusHours(2)) + .arrivalTime(LocalDateTime.now().plusHours(3)) + .price(120.00) + .build(); + + FlightEntity flightEntity2 = FlightEntity.builder() + .id(UUID.randomUUID().toString()) + .fromAirport(AirportEntity.builder().name("From Airport 2").cityName("City A2").build()) + .toAirport(AirportEntity.builder().name("To Airport 2").cityName("City B2").build()) + .departureTime(LocalDateTime.now().plusHours(4)) + .arrivalTime(LocalDateTime.now().plusHours(5)) + .price(200.00) + .build(); + + List flightEntities = Arrays.asList(flightEntity1, flightEntity2); + List result = mapper.map(flightEntities); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(flightEntity1.getId(), result.get(0).getId()); + assertEquals(flightEntity2.getId(), result.get(1).getId()); + assertEquals(flightEntity1.getFromAirport().getName(), result.get(0).getFromAirport().getName()); + assertEquals(flightEntity2.getFromAirport().getName(), result.get(1).getFromAirport().getName()); + + } + + @Test + void testMapWithEdgeCaseValues() { + + // Test edge case with empty or null values + FlightEntity flightEntity = FlightEntity.builder() + .fromAirport(AirportEntity.builder().name("").cityName(null).build()) + .toAirport(AirportEntity.builder().name(null).cityName("").build()) + .departureTime(null) + .arrivalTime(null) + .price(null) + .build(); + + Flight result = mapper.map(flightEntity); + + assertNotNull(result); + assertEquals("", result.getFromAirport().getName()); + assertNull(result.getFromAirport().getCityName()); + assertNull(result.getToAirport().getName()); + assertEquals("", result.getToAirport().getCityName()); + assertNull(result.getDepartureTime()); + assertNull(result.getArrivalTime()); + assertNull(result.getPrice()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/FlightToFlightResponseMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/FlightToFlightResponseMapperTest.java new file mode 100644 index 000000000..583fa9d24 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/FlightToFlightResponseMapperTest.java @@ -0,0 +1,146 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.response.flight.FlightResponse; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link FlightToFlightResponseMapper}. + * This class tests the functionality of the {@link FlightToFlightResponseMapper}, + * ensuring that {@link Flight} objects are correctly mapped to {@link FlightResponse} objects. + * The tests verify that all fields are properly transformed and no data is lost or incorrectly + * mapped during the conversion process. + */ +class FlightToFlightResponseMapperTest { + + private final FlightToFlightResponseMapper mapper = FlightToFlightResponseMapper.initialize(); + + @Test + void testMapFlightNull() { + + FlightResponse result = mapper.map((Flight) null); + + assertNull(result); + + } + + @Test + void testMapFlightCollectionNull() { + + List result = mapper.map((Collection) null); + + assertNull(result); + + } + + @Test + void testMapFlightListEmpty() { + + List result = mapper.map(Collections.emptyList()); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMapFlightListWithNullElements() { + + List flights = Arrays.asList(new Flight(), null); + + List result = mapper.map(flights); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNull(result.get(1)); + } + + @Test + void testMapSingleFlight() { + + Flight flight = Flight.builder() + .id("flight-id-123") + .fromAirport(Airport.builder().id("from-airport-id").name("From Airport").build()) + .toAirport(Airport.builder().id("to-airport-id").name("To Airport").build()) + .departureTime(LocalDateTime.of(2025, 1, 1, 10, 30, 0)) + .arrivalTime(LocalDateTime.of(2025, 1, 1, 14, 30, 0)) + .price(100.0) + .build(); + + FlightResponse result = mapper.map(flight); + + assertNotNull(result); + assertEquals(flight.getId(), result.getId()); + assertEquals(flight.getFromAirport().getId(), result.getFromAirport().getId()); + assertEquals(flight.getToAirport().getId(), result.getToAirport().getId()); + assertEquals(flight.getDepartureTime(), result.getDepartureTime()); + assertEquals(flight.getArrivalTime(), result.getArrivalTime()); + assertEquals(flight.getPrice(), result.getPrice()); + + } + + @Test + void testMapFlightListWithValues() { + + Flight flight1 = Flight.builder() + .id("flight-id-1") + .fromAirport(Airport.builder().id("from-airport-id-1").name("From Airport 1").build()) + .toAirport(Airport.builder().id("to-airport-id-1").name("To Airport 1").build()) + .departureTime(LocalDateTime.of(2025, 1, 1, 10, 30, 0)) + .arrivalTime(LocalDateTime.of(2025, 1, 1, 14, 30, 0)) + .price(100.0) + .build(); + + Flight flight2 = Flight.builder() + .id("flight-id-2") + .fromAirport(Airport.builder().id("from-airport-id-2").name("From Airport 2").build()) + .toAirport(Airport.builder().id("to-airport-id-2").name("To Airport 2").build()) + .departureTime(LocalDateTime.of(2025, 2, 1, 12, 30, 0)) + .arrivalTime(LocalDateTime.of(2025, 2, 1, 16, 30, 0)) + .price(200.0) + .build(); + + List flights = Arrays.asList(flight1, flight2); + List result = mapper.map(flights); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(flight1.getId(), result.get(0).getId()); + assertEquals(flight2.getId(), result.get(1).getId()); + + } + + @Test + void testMapWithEdgeCaseValues() { + + Flight flight = Flight.builder() + .id("") + .fromAirport(Airport.builder().id("").name("").build()) + .toAirport(Airport.builder().id("").name("").build()) + .departureTime(null) + .arrivalTime(null) + .price(0.0) + .build(); + + FlightResponse result = mapper.map(flight); + + assertNotNull(result); + assertEquals("", result.getId()); + assertEquals("", result.getFromAirport().getId()); + assertEquals("", result.getToAirport().getId()); + assertNull(result.getDepartureTime()); + assertNull(result.getArrivalTime()); + assertEquals(0.0, result.getPrice()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/ListFlightEntityToListFlightMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/ListFlightEntityToListFlightMapperTest.java new file mode 100644 index 000000000..4bd4c37b4 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/ListFlightEntityToListFlightMapperTest.java @@ -0,0 +1,46 @@ +package com.example.demo.flight.model.mapper.flight; + +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.entity.FlightEntity; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link ListFlightEntityToListFlightMapper}. + * This class tests the functionality of the {@link ListFlightEntityToListFlightMapper}, + * ensuring that a list of {@link FlightEntity} objects is correctly mapped to a list of {@link Flight} objects. + * The tests validate that all fields are properly transformed for each entity in the list and that + * the mapping maintains data integrity across the collection. + */ +class ListFlightEntityToListFlightMapperTest { + + private final ListFlightEntityToListFlightMapper mapper = ListFlightEntityToListFlightMapper.initialize(); + + @Test + void testToFlightList_WhenFlightEntitiesIsNull() { + + List flightEntities = null; + + List result = mapper.toFlightList(flightEntities); + + assertNull(result); + + } + + @Test + void testToFlightList_WhenFlightEntitiesIsEmpty() { + + List flightEntities = Collections.emptyList(); + + List result = mapper.toFlightList(flightEntities); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/UpdateFlightRequestToFlightEntityMapperTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/UpdateFlightRequestToFlightEntityMapperTest.java new file mode 100644 index 000000000..f3e5a100e --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/model/mapper/flight/UpdateFlightRequestToFlightEntityMapperTest.java @@ -0,0 +1,197 @@ +package com.example.demo.flight.model.mapper.flight; + + +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link UpdateFlightRequestToFlightEntityMapper}. + * This class tests the functionality of mapping between {@link UpdateFlightRequest} + * and {@link FlightEntity} using the {@link UpdateFlightRequestToFlightEntityMapper}. + */ +class UpdateFlightRequestToFlightEntityMapperTest { + + private final UpdateFlightRequestToFlightEntityMapper mapper = UpdateFlightRequestToFlightEntityMapper.initialize(); + + @Test + void testMap_WhenUpdateFlightRequestIsNull() { + + FlightEntity result = mapper.map((UpdateFlightRequest) null); + assertNull(result); + + } + + @Test + void testMap_WhenUpdateFlightRequestIsValid() { + + // Arrange: Create an UpdateFlightRequest + String fromAirportId = UUID.randomUUID().toString(); + String toAirportId = UUID.randomUUID().toString(); + UpdateFlightRequest request = UpdateFlightRequest.builder() + .fromAirportId(fromAirportId) + .toAirportId(toAirportId) + .departureTime(LocalDateTime.of(2024, 1, 18, 10, 30)) + .arrivalTime(LocalDateTime.of(2024, 1, 18, 14, 45)) + .price(299.99) + .build(); + + // Act: Map the request to a FlightEntity + FlightEntity result = mapper.map(request); + + // Assert: Validate the mapped FlightEntity + assertNotNull(result, "Resulting FlightEntity should not be null"); + + // Verify departureTime, arrivalTime, and price + assertEquals(request.getDepartureTime(), result.getDepartureTime(), "Departure Time mismatch"); + assertEquals(request.getArrivalTime(), result.getArrivalTime(), "Arrival Time mismatch"); + assertEquals(request.getPrice(), result.getPrice(), "Price mismatch"); + + } + + @Test + void testMap_WhenUpdateFlightRequestIsEmpty() { + + UpdateFlightRequest updateFlightRequest = new UpdateFlightRequest( + null, null, null, null, null + ); + + FlightEntity result = mapper.map(updateFlightRequest); + + assertNotNull(result); + assertNull(result.getFromAirport()); + assertNull(result.getToAirport()); + assertNull(result.getDepartureTime()); + assertNull(result.getArrivalTime()); + assertNull(result.getPrice()); + + } + + @Test + void testMap_WhenUpdateFlightRequestCollectionIsNull() { + + List updateFlightRequests = null; + + List result = mapper.map(updateFlightRequests); + + assertNull(result); + + } + + @Test + void testMap_WhenUpdateFlightRequestCollectionIsEmpty() { + + List updateFlightRequests = Collections.emptyList(); + + List result = mapper.map(updateFlightRequests); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + } + + @Test + void testMap_WhenUpdateFlightRequestCollectionHasValidElements() { + + // Arrange: Create UpdateFlightRequest objects + String fromAirportId1 = UUID.randomUUID().toString(); + String toAirportId1 = UUID.randomUUID().toString(); + UpdateFlightRequest updateFlightRequest1 = UpdateFlightRequest.builder() + .fromAirportId(fromAirportId1) + .toAirportId(toAirportId1) + .departureTime(LocalDateTime.of(2024, 1, 18, 10, 30)) + .arrivalTime(LocalDateTime.of(2024, 1, 18, 14, 45)) + .price(299.99) + .build(); + + String fromAirportId2 = UUID.randomUUID().toString(); + String toAirportId2 = UUID.randomUUID().toString(); + UpdateFlightRequest updateFlightRequest2 = UpdateFlightRequest.builder() + .fromAirportId(fromAirportId2) + .toAirportId(toAirportId2) + .departureTime(LocalDateTime.of(2024, 1, 19, 12, 15)) + .arrivalTime(LocalDateTime.of(2024, 1, 19, 16, 45)) + .price(350.75) + .build(); + + List updateFlightRequests = Arrays.asList(updateFlightRequest1, updateFlightRequest2); + + // Act: Map the list of UpdateFlightRequest objects to FlightEntity objects + List result = mapper.map(updateFlightRequests); + + // Assert: Validate the results + assertNotNull(result, "The resulting list of FlightEntity objects should not be null"); + assertEquals(2, result.size(), "The size of the resulting list should match the input list size"); + + // Validate the first FlightEntity + FlightEntity flight1 = result.get(0); + assertNotNull(flight1, "First FlightEntity should not be null"); + assertEquals(updateFlightRequest1.getDepartureTime(), flight1.getDepartureTime(), "Departure time mismatch for flight 1"); + assertEquals(updateFlightRequest1.getArrivalTime(), flight1.getArrivalTime(), "Arrival time mismatch for flight 1"); + assertEquals(updateFlightRequest1.getPrice(), flight1.getPrice(), "Price mismatch for flight 1"); + + // Validate the second FlightEntity + FlightEntity flight2 = result.get(1); + assertNotNull(flight2, "Second FlightEntity should not be null"); + assertEquals(updateFlightRequest2.getDepartureTime(), flight2.getDepartureTime(), "Departure time mismatch for flight 2"); + assertEquals(updateFlightRequest2.getArrivalTime(), flight2.getArrivalTime(), "Arrival time mismatch for flight 2"); + assertEquals(updateFlightRequest2.getPrice(), flight2.getPrice(), "Price mismatch for flight 2"); + + } + + @Test + void testUpdateFlightMapper_WhenFlightEntityIsUpdated() { + + // Create AirportEntities using Builder pattern + AirportEntity departureAirportEntity = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Departure Airport") + .cityName("Departure City") + .build(); + + AirportEntity arrivalAirportEntity = AirportEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Arrival Airport") + .cityName("Arrival City") + .build(); + + // Create a flight entity and update request + FlightEntity flightEntity = new FlightEntity(); + UpdateFlightRequest request = UpdateFlightRequest.builder() + .fromAirportId(departureAirportEntity.getId()) + .toAirportId(arrivalAirportEntity.getId()) + .departureTime(LocalDateTime.of(2024, 1, 18, 10, 30)) + .arrivalTime(LocalDateTime.of(2024, 1, 18, 14, 45)) + .price(299.99) + .build(); + + // Call the updateFlightMapper method + mapper.updateFlightMapper(flightEntity, request, departureAirportEntity, arrivalAirportEntity); + + // Assert: Validate that the flight entity has been updated correctly + assertNotNull(flightEntity.getFromAirport(), "From Airport should not be null"); + assertNotNull(flightEntity.getToAirport(), "To Airport should not be null"); + + // Validate mapping of fromAirport and toAirport + assertEquals("Departure Airport", flightEntity.getFromAirport().getName(), "From Airport Name mismatch"); + assertEquals("Departure City", flightEntity.getFromAirport().getCityName(), "From Airport City mismatch"); + assertEquals("Arrival Airport", flightEntity.getToAirport().getName(), "To Airport Name mismatch"); + assertEquals("Arrival City", flightEntity.getToAirport().getCityName(), "To Airport City mismatch"); + + // Validate the other properties + assertEquals(request.getDepartureTime(), flightEntity.getDepartureTime(), "Departure Time mismatch"); + assertEquals(request.getArrivalTime(), flightEntity.getArrivalTime(), "Arrival Time mismatch"); + assertEquals(request.getPrice(), flightEntity.getPrice(), "Price mismatch"); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportCreateServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportCreateServiceImplTest.java new file mode 100644 index 000000000..26fafd566 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportCreateServiceImplTest.java @@ -0,0 +1,86 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.CreateAirportRequestBuilder; +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.mapper.airport.AirportEntityToAirportMapper; +import com.example.demo.flight.model.mapper.airport.CreateAirportRequestToAirportEntityMapper; +import com.example.demo.flight.repository.AirportRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link AirportCreateServiceImpl}. + * This class verifies the correctness of the business logic in {@link AirportCreateServiceImpl}. + */ +class AirportCreateServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private AirportCreateServiceImpl airportCreateService; + + @Mock + private AirportRepository airportRepository; + + private final CreateAirportRequestToAirportEntityMapper createAirportRequestToAirportEntityMapper = + CreateAirportRequestToAirportEntityMapper.initialize(); + + private final AirportEntityToAirportMapper airportEntityToAirportMapper = + AirportEntityToAirportMapper.initialize(); + + @Test + void givenValidCreateAirportRequest_whenCreateAirport_ThenReturnAirport(){ + + // Given + final CreateAirportRequest request = new CreateAirportRequestBuilder() + .withValidFields().build(); + + final AirportEntity mockAirportEntity = createAirportRequestToAirportEntityMapper.mapForSaving(request); + + final Airport mockAirport = airportEntityToAirportMapper.map(mockAirportEntity); + + // When + when(airportRepository.existsByName(anyString())).thenReturn(false); + when(airportRepository.save(any(AirportEntity.class))).thenReturn(mockAirportEntity); + + // Then + Airport response = airportCreateService.createAirport(request); + + assertEquals(mockAirport.getName(),response.getName()); + + // Verify + verify(airportRepository,times(1)).save(any(AirportEntity.class)); + verify(airportRepository, times(1)).existsByName(anyString()); + + } + + @Test + void givenValidCreateAirportRequest_whenCreateAirportRequest_ThenThrowAirportNameAlreadyExistException(){ + + // Given + final CreateAirportRequest request = new CreateAirportRequestBuilder() + .withValidFields().build(); + + // When + when(airportRepository.existsByName(request.getName())).thenReturn(true); + + // Then + assertThrows(AirportNameAlreadyExistException.class,()-> airportCreateService.createAirport(request)); + + // Verify + verify(airportRepository,times(1)).existsByName(anyString()); + verify(airportRepository,times(0)).save(any(AirportEntity.class)); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportDeleteServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportDeleteServiceImplTest.java new file mode 100644 index 000000000..845750214 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportDeleteServiceImplTest.java @@ -0,0 +1,71 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.repository.AirportRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link AirportDeleteServiceImpl}. + * This class verifies the correctness of the business logic in {@link AirportDeleteServiceImpl}. + */ +class AirportDeleteServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private AirportDeleteServiceImpl airportDeleteService; + + @Mock + private AirportRepository airportRepository; + + + @Test + void givenValidAirportId_whenDeleteAirportById_thenDeleteAirportSuccessfully() { + + // Given + final String mockId = UUID.randomUUID().toString(); + final AirportEntity mockEntity = new AirportEntityBuilder() + .withId(mockId) + .build(); + + // When + when(airportRepository.findById(mockId)).thenReturn(java.util.Optional.of(mockEntity)); + + // Then + airportDeleteService.deleteAirportById(mockId); + + // Verify + verify(airportRepository, times(1)).findById(mockId); + verify(airportRepository, times(1)).delete(mockEntity); + + } + + @Test + void givenInvalidAirportId_whenDeleteAirportById_thenThrowAirportNotFoundException() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + // When + when(airportRepository.findById(mockId)).thenReturn(Optional.empty()); + + // Then + assertThrows(AirportNotFoundException.class, () -> + airportDeleteService.deleteAirportById(mockId) + ); + + // Verify + verify(airportRepository, times(1)).findById(mockId); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportReadServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportReadServiceImplTest.java new file mode 100644 index 000000000..235784ea6 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportReadServiceImplTest.java @@ -0,0 +1,155 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.AirportPagingRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.mapper.airport.AirportEntityToAirportMapper; +import com.example.demo.flight.model.mapper.airport.ListAirportEntityToListAirportMapper; +import com.example.demo.flight.repository.AirportRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link AirportReadServiceImpl}. + * This class verifies the correctness of the business logic in {@link AirportReadServiceImpl}. + */ +class AirportReadServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private AirportReadServiceImpl airportReadService; + + @Mock + private AirportRepository airportRepository; + + private final AirportEntityToAirportMapper airportEntityToAirportMapper = + AirportEntityToAirportMapper.initialize(); + + private final ListAirportEntityToListAirportMapper listAirportEntityToListAirportMapper = + ListAirportEntityToListAirportMapper.initialize(); + + @Test + void givenExistAirportId_whenGetAirportById_thenReturnAirport() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + final AirportEntity airportEntity = new AirportEntityBuilder() + .withId(mockId) + .withValidFields(); + + final Airport mockAirport = airportEntityToAirportMapper.map(airportEntity); + + // When + when(airportRepository.findById(mockId)).thenReturn(Optional.of(airportEntity)); + + // Then + final Airport expected = airportReadService.getAirportById(mockId); + + assertNotNull(expected); + assertEquals(mockAirport.getId(),expected.getId()); + assertEquals(mockAirport.getName(),expected.getName()); + + // Verify + verify(airportRepository, times(1)).findById(mockId); + + } + + @Test + void givenNonExistId_whenGetById_thenThrowTaskNotFoundException() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + // When + when(airportRepository.findById(mockId)).thenReturn(Optional.empty()); + + // Then + assertThrows(AirportNotFoundException.class, + ()->airportReadService.getAirportById(mockId)); + + // Verify + verify(airportRepository, times(1)).findById(mockId); + + } + + @Test + void givenTaskPagingRequest_WhenTaskPageList_ThenReturnCustomPageTaskList() { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + final AirportEntity airportEntity = new AirportEntityBuilder().withValidFields(); + + Page airportEntityPage = new PageImpl<>(Collections.singletonList(airportEntity)); + + List products = listAirportEntityToListAirportMapper.toAirportList(airportEntityPage.getContent()); + + CustomPage expected = CustomPage.of(products, airportEntityPage); + + // When + when(airportRepository.findAll(any(Pageable.class))).thenReturn(airportEntityPage); + + // Then + CustomPage result = airportReadService.getAllAirports(pagingRequest); + + assertNotNull(result); + assertFalse(result.getContent().isEmpty()); + assertEquals(expected.getPageNumber(), result.getPageNumber()); + assertEquals(expected.getContent().get(0).getId(), result.getContent().get(0).getId()); + assertEquals(expected.getTotalPageCount(), result.getTotalPageCount()); + assertEquals(expected.getTotalElementCount(), result.getTotalElementCount()); + + // Verify + verify(airportRepository, times(1)).findAll(any(Pageable.class)); + + } + + @Test + void givenTaskPagingRequest_WhenNoTaskPageList_ThenThrowTaskNotFoundException() { + + // Given + final AirportPagingRequest pagingRequest = AirportPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + Page airportEntityPage = new PageImpl<>(Collections.emptyList()); + + // When + when(airportRepository.findAll(any(Pageable.class))).thenReturn(airportEntityPage); + + // Then + assertThrows(AirportNotFoundException.class, () -> airportReadService.getAllAirports(pagingRequest)); + + // Verify + verify(airportRepository, times(1)).findAll(any(Pageable.class)); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportServiceImplTest.java new file mode 100644 index 000000000..ca1c3666b --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportServiceImplTest.java @@ -0,0 +1,202 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AirportBuilder; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.builder.CreateAirportRequestBuilder; +import com.example.demo.builder.UpdateAirportRequestBuilder; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.CreateAirportRequest; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.service.airport.AirportCreateService; +import com.example.demo.flight.service.airport.AirportDeleteService; +import com.example.demo.flight.service.airport.AirportReadService; +import com.example.demo.flight.service.airport.AirportUpdateService; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.PageRequest; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link AirportServiceImpl}. + * This class verifies the correctness of the business logic in {@link AirportServiceImpl}. + */ +class AirportServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private AirportServiceImpl airportService; + + @Mock + private AirportCreateService airportCreateService; + + @Mock + private AirportReadService airportReadService; + + @Mock + private AirportUpdateService airportUpdateService; + + @Mock + private AirportDeleteService airportDeleteService; + + @Test + void givenValidCreateAirportRequest_whenCreateAirport_thenReturnCreatedAirport() { + + // Given + final CreateAirportRequest request = new CreateAirportRequestBuilder() + .withValidFields().build(); + + Airport expectedAirport = Airport.builder() + .id(UUID.randomUUID().toString()) + .name(request.getName()) + .cityName(request.getCityName()) + .build(); + + // When + when(airportCreateService.createAirport(any(CreateAirportRequest.class))).thenReturn(expectedAirport); + + // Then + Airport result = airportService.createAirport(request); + + assertNotNull(result); + assertEquals(expectedAirport.getId(), result.getId()); + assertEquals(expectedAirport.getName(), result.getName()); + assertEquals(expectedAirport.getCityName(), result.getCityName()); + + // Verify + verify(airportCreateService, times(1)).createAirport(any(CreateAirportRequest.class)); + + } + + @Test + void givenValidAirportId_whenGetAirportById_thenReturnAirport() { + + // Given + String airportId = UUID.randomUUID().toString(); + Airport expectedAirport = Airport.builder() + .id(airportId) + .name("Test Airport") + .cityName("Test City") + .build(); + + // When + when(airportReadService.getAirportById(airportId)).thenReturn(expectedAirport); + + // Then + Airport result = airportService.getAirportById(airportId); + + assertNotNull(result); + assertEquals(expectedAirport.getId(), result.getId()); + assertEquals(expectedAirport.getName(), result.getName()); + assertEquals(expectedAirport.getCityName(), result.getCityName()); + + // Verify + verify(airportReadService, times(1)).getAirportById(airportId); + + } + + @Test + void givenValidPagingRequest_whenGetAllAirports_thenReturnCustomPageOfAirports() { + + // Given + CustomPaging paging = CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build(); + + CustomPagingRequest pagingRequest = mock(CustomPagingRequest.class); + + List airportList = List.of(new Airport()); + CustomPage expectedPage = CustomPage.builder() + .content(airportList) + .pageNumber(1) + .pageSize(1) + .totalElementCount(1L) + .totalPageCount(1) + .build(); + + + // When + when(pagingRequest.getPagination()).thenReturn(paging); + when(pagingRequest.toPageable()).thenReturn(PageRequest.of(paging.getPageNumber(), paging.getPageSize())); + when(airportReadService.getAllAirports(pagingRequest)).thenReturn(expectedPage); + + // Then + CustomPage result = airportService.getAllAirports(pagingRequest); + + assertNotNull(result); + assertEquals(expectedPage, result); + assertEquals(1, result.getContent().size()); + + // Verify + verify(airportReadService, times(1)).getAllAirports(pagingRequest); + + } + + @Test + void givenUpdateAirportRequest_whenUpdateAirportById_thenSuccess() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateAirportRequest mockUpdateAirportRequest = new UpdateAirportRequestBuilder() + .withValidFields() + .build(); + + final AirportEntity updatedAirportEntity = new AirportEntityBuilder() + .withId(mockId) + .withName(mockUpdateAirportRequest.getName()) + .withCityName(mockUpdateAirportRequest.getCityName()) + .build(); + + final Airport expected = new AirportBuilder() + .withId(updatedAirportEntity.getId()) + .withName(updatedAirportEntity.getName()) + .withCityName(updatedAirportEntity.getCityName()) + .build(); + + when(airportUpdateService.updateAirportById(mockId, mockUpdateAirportRequest)).thenReturn(expected); + + // When + Airport result = airportService.updateAirportById(mockId, mockUpdateAirportRequest); + + // Then + assertNotNull(result); + assertEquals(expected.getName(), result.getName()); + assertEquals(expected.getCityName(), result.getCityName()); + + // Verify + verify(airportUpdateService,times(1)).updateAirportById(mockId, mockUpdateAirportRequest); + + } + + @Test + void givenValidAirportId_whenDeleteAirportById_thenDeleteAirportSuccessfully() { + + // Given + final String airportId = UUID.randomUUID().toString(); + + // When + doNothing().when(airportDeleteService).deleteAirportById(airportId); + + // Then + airportService.deleteAirportById(airportId); + + // Verify + verify(airportDeleteService, times(1)).deleteAirportById(airportId); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportUpdateServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportUpdateServiceImplTest.java new file mode 100644 index 000000000..715c74eae --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/airport/impl/AirportUpdateServiceImplTest.java @@ -0,0 +1,125 @@ +package com.example.demo.flight.service.airport.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.builder.UpdateAirportRequestBuilder; +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.dto.request.airport.UpdateAirportRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.mapper.airport.AirportEntityToAirportMapper; +import com.example.demo.flight.repository.AirportRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link AirportUpdateServiceImpl}. + * This class verifies the correctness of the business logic in {@link AirportUpdateServiceImpl}. + */ +class AirportUpdateServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private AirportUpdateServiceImpl airportUpdateService; + + @Mock + private AirportRepository airportRepository; + + private final AirportEntityToAirportMapper airportEntityToAirportMapper = + AirportEntityToAirportMapper.initialize(); + + @Test + void givenUpdateAirportRequest_whenUpdateAirportById_thenSuccess() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + final UpdateAirportRequest mockUpdateAirportRequest = new UpdateAirportRequestBuilder() + .withValidFields() + .build(); + + final AirportEntity existingAirportEntity = new AirportEntityBuilder() + .withId(mockId) + .withValidFields(); + + final AirportEntity updatedAirportEntity = new AirportEntityBuilder() + .withId(mockId) + .withName(mockUpdateAirportRequest.getName()) + .withCityName(mockUpdateAirportRequest.getCityName()) + .build(); + + final Airport expected = airportEntityToAirportMapper.map(updatedAirportEntity); + + // When + when(airportRepository.findById(mockId)).thenReturn(Optional.of(existingAirportEntity)); + when(airportRepository.save(existingAirportEntity)).thenReturn(updatedAirportEntity); + + Airport result = airportUpdateService.updateAirportById(mockId, mockUpdateAirportRequest); + + // Then + assertNotNull(result); + assertEquals(expected.getName(), result.getName()); + assertEquals(expected.getCityName(), result.getCityName()); + + // Verify + verify(airportRepository,times(1)).findById(mockId); + verify(airportRepository,times(1)).save(existingAirportEntity); + + } + + @Test + void givenNonexistentAirportId_whenUpdateAirportById_thenThrowAirportNotFoundException() { + + // Given + final String airportId = UUID.randomUUID().toString(); + + final UpdateAirportRequest updateRequest = new UpdateAirportRequestBuilder() + .withValidFields() + .build(); + + // When + when(airportRepository.findById(airportId)).thenReturn(Optional.empty()); + + // Then + assertThrows(AirportNotFoundException.class, () -> + airportUpdateService.updateAirportById(airportId, updateRequest)); + + // Verify + verify(airportRepository, times(1)).findById(airportId); + + } + + @Test + void givenDuplicateAirportName_whenUpdateAirportById_thenThrowIllegalArgumentException() { + + // Given + final String airportId = UUID.randomUUID().toString(); + final UpdateAirportRequest updateRequest = new UpdateAirportRequestBuilder() + .withName("Duplicate Name") + .withCityName("New Location") + .build(); + + // When + when(airportRepository.existsByName(updateRequest.getName())).thenReturn(true); + + // Then + AirportNameAlreadyExistException exception = assertThrows(AirportNameAlreadyExistException.class, () -> + airportUpdateService.updateAirportById(airportId, updateRequest)); + + assertEquals("Airport with this name already exist\n" + + " With given airport name = Duplicate Name", exception.getMessage()); + + // Verify + verify(airportRepository,times(0)).findById(airportId); + verify(airportRepository,times(1)).existsByName(updateRequest.getName()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightCreateServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightCreateServiceImplTest.java new file mode 100644 index 000000000..b451f2919 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightCreateServiceImplTest.java @@ -0,0 +1,131 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.CreateFlightRequestBuilder; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.CreateFlightRequestToFlightEntityMapper; +import com.example.demo.flight.model.mapper.flight.FlightEntityToFlightMapper; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.repository.FlightRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link FlightCreateServiceImpl}. + * This class verifies the correctness of the business logic in {@link FlightCreateServiceImpl}. + */ +class FlightCreateServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private FlightCreateServiceImpl flightCreateService; + + @Mock + private FlightRepository flightRepository; + + @Mock + private AirportRepository airportRepository; + + private final CreateFlightRequestToFlightEntityMapper createFlightRequestToFlightEntityMapper = + CreateFlightRequestToFlightEntityMapper.initialize(); + + private final FlightEntityToFlightMapper flightEntityToFlightMapper = + FlightEntityToFlightMapper.initialize(); + + @Test + void givenValidCreateFlightRequest_whenCreateFlight_ThenReturnFlight() { + + // Given + final CreateFlightRequest request = new CreateFlightRequestBuilder() + .withValidFields().build(); + + final AirportEntity departureAirport = new AirportEntity("AIRPORT1", "Airport Name", "City Name"); + final AirportEntity arrivalAirport = new AirportEntity("AIRPORT2", "Destination Name", "City Name"); + + final FlightEntity mockFlightEntity = createFlightRequestToFlightEntityMapper + .mapForSaving(request, departureAirport, arrivalAirport); + + final Flight mockFlight = flightEntityToFlightMapper.map(mockFlightEntity); + + // When + when(airportRepository.findById(request.getFromAirportId())).thenReturn(Optional.of(departureAirport)); + when(airportRepository.findById(request.getToAirportId())).thenReturn(Optional.of(arrivalAirport)); + when(flightRepository.save(any(FlightEntity.class))).thenReturn(mockFlightEntity); + + // Then + Flight response = flightCreateService.createFlight(request); + + assertEquals(mockFlight.getId(), response.getId()); + assertEquals(mockFlight.getFromAirport().getId(), response.getFromAirport().getId()); + assertEquals(mockFlight.getToAirport().getId(), response.getToAirport().getId()); + assertEquals(mockFlight.getDepartureTime(), response.getDepartureTime()); + assertEquals(mockFlight.getArrivalTime(), response.getArrivalTime()); + assertEquals(mockFlight.getPrice(), response.getPrice()); + + // Verify + verify(airportRepository, times(1)).findById(request.getFromAirportId()); + verify(airportRepository, times(1)).findById(request.getToAirportId()); + verify(flightRepository, times(1)).save(any(FlightEntity.class)); + + } + + @Test + void givenNonExistentDepartureAirport_whenCreateFlight_ThenThrowAirportNotFoundException() { + + // Given + final CreateFlightRequest request = new CreateFlightRequestBuilder() + .withValidFields().build(); + + // When + when(airportRepository.findById(request.getFromAirportId())).thenReturn(Optional.empty()); + + // Then + AirportNotFoundException exception = assertThrows(AirportNotFoundException.class, () -> + flightCreateService.createFlight(request) + ); + assertEquals("Airport not found!\n" + + " Departure airport not found with id AIRPORT1", exception.getMessage()); + + // Verify + verify(airportRepository, times(1)).findById(request.getFromAirportId()); + verify(airportRepository, times(0)).findById(request.getToAirportId()); + } + + @Test + void givenNonExistentArrivalAirport_whenCreateFlight_ThenThrowAirportNotFoundException() { + + // Given + final CreateFlightRequest request = new CreateFlightRequestBuilder() + .withValidFields().build(); + + final AirportEntity departureAirport = new AirportEntity("AIRPORT1", "Airport Name", "City Name"); + + // When + when(airportRepository.findById(request.getFromAirportId())).thenReturn(Optional.of(departureAirport)); + when(airportRepository.findById(request.getToAirportId())).thenReturn(Optional.empty()); + + // Then + AirportNotFoundException exception = assertThrows(AirportNotFoundException.class, () -> + flightCreateService.createFlight(request) + ); + assertEquals("Airport not found!\n" + + " Arrival airport not found with id AIRPORT2", exception.getMessage()); + + // Verify + verify(airportRepository, times(1)).findById(request.getFromAirportId()); + verify(airportRepository, times(1)).findById(request.getToAirportId()); + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightDeleteServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightDeleteServiceImplTest.java new file mode 100644 index 000000000..35a19863c --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightDeleteServiceImplTest.java @@ -0,0 +1,72 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.FlightEntityBuilder; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.repository.FlightRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link FlightDeleteServiceImpl}. + * This class verifies the correctness of the business logic in {@link FlightDeleteServiceImpl}. + */ +class FlightDeleteServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private FlightDeleteServiceImpl flightDeleteService; + + @Mock + private FlightRepository flightRepository; + + @Test + void givenValidFlightId_whenDeleteFlightById_thenDeleteFlightSuccessfully() { + + // Given + final String mockId = UUID.randomUUID().toString(); + final FlightEntity mockEntity = new FlightEntityBuilder() + .withId(mockId) + .withValidFields() + .build(); + + // When + when(flightRepository.findById(mockId)).thenReturn(Optional.of(mockEntity)); + + // Then + flightDeleteService.deleteFlightById(mockId); + + // Verify + verify(flightRepository, times(1)).findById(mockId); + verify(flightRepository, times(1)).delete(mockEntity); + + } + + @Test + void givenInvalidFlightId_whenDeleteFlightById_thenThrowFlightNotFoundException() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + // When + when(flightRepository.findById(mockId)).thenReturn(Optional.empty()); + + // Then + assertThrows(FlightNotFoundException.class, () -> + flightDeleteService.deleteFlightById(mockId) + ); + + // Verify + verify(flightRepository, times(1)).findById(mockId); + verify(flightRepository, never()).delete(any()); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightReadServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightReadServiceImplTest.java new file mode 100644 index 000000000..597548399 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightReadServiceImplTest.java @@ -0,0 +1,160 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.FlightEntityBuilder; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.FlightPagingRequest; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.FlightEntityToFlightMapper; +import com.example.demo.flight.model.mapper.flight.ListFlightEntityToListFlightMapper; +import com.example.demo.flight.repository.FlightRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link FlightReadServiceImpl}. + * This class verifies the correctness of the business logic in {@link FlightReadServiceImpl}. + */ +class FlightReadServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private FlightReadServiceImpl flightReadService; + + @Mock + private FlightRepository flightRepository; + + private final FlightEntityToFlightMapper flightEntityToFlightMapper = + FlightEntityToFlightMapper.initialize(); + + private final ListFlightEntityToListFlightMapper listFlightEntityToListFlightMapper = + ListFlightEntityToListFlightMapper.initialize(); + + @Test + void givenExistFlightId_whenGetFlightById_thenReturnFlight() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + final FlightEntity flightEntity = new FlightEntityBuilder() + .withId(mockId) + .withValidFields() + .build(); + + final Flight mockFlight = flightEntityToFlightMapper.map(flightEntity); + + // When + when(flightRepository.findById(mockId)).thenReturn(Optional.of(flightEntity)); + + // Then + final Flight expected = flightReadService.getFlightById(mockId); + + assertNotNull(expected); + assertEquals(mockFlight.getId(), expected.getId()); + assertEquals(mockFlight.getToAirport().getName(), expected.getToAirport().getName()); + assertEquals(mockFlight.getFromAirport().getName(), expected.getFromAirport().getName()); + assertEquals(mockFlight.getArrivalTime(), expected.getArrivalTime()); + assertEquals(mockFlight.getDepartureTime(), expected.getDepartureTime()); + assertEquals(mockFlight.getPrice(), expected.getPrice()); + + // Verify + verify(flightRepository, times(1)).findById(mockId); + } + + @Test + void givenNonExistFlightId_whenGetFlightById_thenThrowFlightNotFoundException() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + // When + when(flightRepository.findById(mockId)).thenReturn(Optional.empty()); + + // Then + assertThrows(FlightNotFoundException.class, + () -> flightReadService.getFlightById(mockId)); + + // Verify + verify(flightRepository, times(1)).findById(mockId); + + } + + @Test + void givenPagingRequest_WhenFlightsAreFound_ThenReturnCustomPageFlightList() { + + // Given + final FlightPagingRequest pagingRequest = FlightPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + final FlightEntity flightEntity = new FlightEntityBuilder().withValidFields().build(); + + Page flightEntityPage = new PageImpl<>(Collections.singletonList(flightEntity)); + + List flights = listFlightEntityToListFlightMapper.toFlightList(flightEntityPage.getContent()); + + CustomPage expected = CustomPage.of(flights, flightEntityPage); + + // When + when(flightRepository.findAll(any(Pageable.class))).thenReturn(flightEntityPage); + + // Then + CustomPage result = flightReadService.getAllFlights(pagingRequest); + + assertNotNull(result); + assertFalse(result.getContent().isEmpty()); + assertEquals(expected.getPageNumber(), result.getPageNumber()); + assertEquals(expected.getContent().get(0).getId(), result.getContent().get(0).getId()); + assertEquals(expected.getTotalPageCount(), result.getTotalPageCount()); + assertEquals(expected.getTotalElementCount(), result.getTotalElementCount()); + + // Verify + verify(flightRepository, times(1)).findAll(any(Pageable.class)); + + } + + @Test + void givenPagingRequest_WhenNoFlightsAreFound_ThenThrowFlightNotFoundException() { + + // Given + final FlightPagingRequest pagingRequest = FlightPagingRequest.builder() + .pagination( + CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build() + ).build(); + + Page flightEntityPage = new PageImpl<>(Collections.emptyList()); + + // When + when(flightRepository.findAll(any(Pageable.class))).thenReturn(flightEntityPage); + + // Then + assertThrows(FlightNotFoundException.class, () -> flightReadService.getAllFlights(pagingRequest)); + + // Verify + verify(flightRepository, times(1)).findAll(any(Pageable.class)); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightServiceImplTest.java new file mode 100644 index 000000000..cbfeb74ec --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightServiceImplTest.java @@ -0,0 +1,230 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AirportBuilder; +import com.example.demo.builder.CreateFlightRequestBuilder; +import com.example.demo.builder.FlightBuilder; +import com.example.demo.builder.UpdateFlightRequestBuilder; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.common.model.dto.request.CustomPagingRequest; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.Airport; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.service.flight.FlightCreateService; +import com.example.demo.flight.service.flight.FlightDeleteService; +import com.example.demo.flight.service.flight.FlightReadService; +import com.example.demo.flight.service.flight.FlightUpdateService; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.PageRequest; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link FlightServiceImpl}. + * This class verifies the correctness of the business logic in {@link FlightServiceImpl}. + */ +class FlightServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private FlightServiceImpl flightService; + + @Mock + private FlightCreateService flightCreateService; + + @Mock + private FlightReadService flightReadService; + + @Mock + private FlightUpdateService flightUpdateService; + + @Mock + private FlightDeleteService flightDeleteService; + + + @Test + void givenValidCreateFlightRequest_whenCreateFlight_ThenReturnFlight() { + + // Given + final CreateFlightRequest request = new CreateFlightRequestBuilder() + .withValidFields() + .build(); + + final Airport departureAirport = new AirportBuilder().withValidFields(); + + final Airport arrivalAirport = new AirportBuilder().withValidFields(); + + final Flight mockFlight = new FlightBuilder().withValidFields() + .withFromAirport(departureAirport) + .withToAirport(arrivalAirport) + .build(); + + // When + when(flightCreateService.createFlight(any(CreateFlightRequest.class))).thenReturn(mockFlight); + + // Then + Flight response = flightService.createFlight(request); + + assertNotNull(response); + assertEquals(mockFlight.getId(), response.getId()); + assertEquals(mockFlight.getFromAirport(), response.getFromAirport()); + assertEquals(mockFlight.getToAirport(), response.getToAirport()); + assertEquals(mockFlight.getDepartureTime(), response.getDepartureTime()); + assertEquals(mockFlight.getArrivalTime(), response.getArrivalTime()); + assertEquals(mockFlight.getPrice(), response.getPrice()); + + // Verify + verify(flightCreateService, times(1)).createFlight(any(CreateFlightRequest.class)); + + } + + @Test + void givenValidFlightId_whenGetFlightById_thenReturnFlight() { + + // Given + final String flightId = "FLIGHT123"; + + final Airport departureAirport = new AirportBuilder().withValidFields(); + + final Airport arrivalAirport = new AirportBuilder().withValidFields(); + + final Flight mockFlight = new FlightBuilder().withValidFields() + .withFromAirport(departureAirport) + .withToAirport(arrivalAirport) + .build(); + + // When + when(flightReadService.getFlightById(flightId)).thenReturn(mockFlight); + + // Then + Flight response = flightService.getFlightById(flightId); + + assertNotNull(response); + assertEquals(mockFlight.getId(), response.getId()); + assertEquals(mockFlight.getFromAirport(), response.getFromAirport()); + assertEquals(mockFlight.getToAirport(), response.getToAirport()); + assertEquals(mockFlight.getDepartureTime(), response.getDepartureTime()); + assertEquals(mockFlight.getArrivalTime(), response.getArrivalTime()); + assertEquals(mockFlight.getPrice(), response.getPrice()); + + // Verify + verify(flightReadService, times(1)).getFlightById(flightId); + } + + @Test + void givenValidPagingRequest_whenGetAllFlights_thenReturnCustomPageOfFlights() { + + // Given + CustomPaging paging = CustomPaging.builder() + .pageSize(1) + .pageNumber(1) + .build(); + + CustomPagingRequest pagingRequest = mock(CustomPagingRequest.class); + + List flightList = List.of(new Flight()); + CustomPage expectedPage = CustomPage.builder() + .content(flightList) + .pageNumber(1) + .pageSize(1) + .totalElementCount(1L) + .totalPageCount(1) + .build(); + + + // When + when(pagingRequest.getPagination()).thenReturn(paging); + when(pagingRequest.toPageable()).thenReturn(PageRequest.of(paging.getPageNumber(), paging.getPageSize())); + when(flightReadService.getAllFlights(pagingRequest)).thenReturn(expectedPage); + + // Then + CustomPage result = flightService.getAllFlights(pagingRequest); + + assertNotNull(result); + assertEquals(expectedPage, result); + assertEquals(1, result.getContent().size()); + + // Verify + verify(flightReadService, times(1)).getAllFlights(pagingRequest); + + } + + @Test + void givenValidUpdateFlightRequest_whenUpdateFlightById_thenSuccess() { + + // Given + final String flightId = UUID.randomUUID().toString(); + final String fromAirportId = UUID.randomUUID().toString(); + final String toAirportId = UUID.randomUUID().toString(); + + final UpdateFlightRequest mockUpdateFlightRequest = new UpdateFlightRequestBuilder() + .withValidFields() + .build(); + + final Airport mockFromAirport = new AirportBuilder() + .withId(fromAirportId) + .withValidFields(); + + final Airport mockToAirport = new AirportBuilder() + .withId(toAirportId) + .withValidFields(); + + final Flight mockUpdatedFlight = Flight.builder() + .id(flightId) + .fromAirport(mockFromAirport) + .toAirport(mockToAirport) + .departureTime(mockUpdateFlightRequest.getDepartureTime()) + .arrivalTime(mockUpdateFlightRequest.getArrivalTime()) + .price(mockUpdateFlightRequest.getPrice()) + .build(); + + // When + when(flightUpdateService.updateFlightById(flightId, mockUpdateFlightRequest)).thenReturn(mockUpdatedFlight); + + // Then + Flight result = flightService.updateFlightById(flightId, mockUpdateFlightRequest); + + assertNotNull(result); + assertEquals(mockUpdatedFlight.getId(), result.getId()); + assertEquals(mockUpdatedFlight.getFromAirport().getId(), result.getFromAirport().getId()); + assertEquals(mockUpdatedFlight.getFromAirport().getName(), result.getFromAirport().getName()); + assertEquals(mockUpdatedFlight.getFromAirport().getCityName(), result.getFromAirport().getCityName()); + assertEquals(mockUpdatedFlight.getToAirport().getId(), result.getToAirport().getId()); + assertEquals(mockUpdatedFlight.getToAirport().getName(), result.getToAirport().getName()); + assertEquals(mockUpdatedFlight.getToAirport().getCityName(), result.getToAirport().getCityName()); + assertEquals(mockUpdatedFlight.getPrice(), result.getPrice()); + + // Verify + verify(flightUpdateService, times(1)).updateFlightById(flightId, mockUpdateFlightRequest); + + } + + @Test + void givenInvalidFlightId_whenDeleteFlightById_thenThrowFlightNotFoundException() { + + // Given + final String mockId = UUID.randomUUID().toString(); + + // When + doThrow(new FlightNotFoundException("Flight not found with id: " + mockId)) + .when(flightDeleteService).deleteFlightById(mockId); + + // Then + assertThrows(FlightNotFoundException.class, () -> flightService.deleteFlightById(mockId)); + + // Verify + verify(flightDeleteService, times(1)).deleteFlightById(mockId); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightUpdateServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightUpdateServiceImplTest.java new file mode 100644 index 000000000..916c420a2 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/FlightUpdateServiceImplTest.java @@ -0,0 +1,180 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.builder.FlightEntityBuilder; +import com.example.demo.builder.UpdateFlightRequestBuilder; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import com.example.demo.flight.model.entity.AirportEntity; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.FlightEntityToFlightMapper; +import com.example.demo.flight.model.mapper.flight.UpdateFlightRequestToFlightEntityMapper; +import com.example.demo.flight.repository.AirportRepository; +import com.example.demo.flight.repository.FlightRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit test class for {@link FlightUpdateServiceImpl}. + * This class verifies the correctness of the business logic in {@link FlightUpdateServiceImpl}. + */ +class FlightUpdateServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private FlightUpdateServiceImpl flightUpdateService; + + @Mock + private FlightRepository flightRepository; + + @Mock + private AirportRepository airportRepository; + + private final UpdateFlightRequestToFlightEntityMapper updateFlightRequestToFlightEntityMapper = + UpdateFlightRequestToFlightEntityMapper.initialize(); + + private final FlightEntityToFlightMapper flightEntityToFlightMapper = + FlightEntityToFlightMapper.initialize(); + + @Test + void givenValidUpdateFlightRequest_whenUpdateFlightById_thenSuccess() { + + // Given + final String flightId = UUID.randomUUID().toString(); + final String fromAirportId = UUID.randomUUID().toString(); + final String toAirportId = UUID.randomUUID().toString(); + + final UpdateFlightRequest mockUpdateFlightRequest = new UpdateFlightRequestBuilder() + .withValidFields() + .withFromAirportId(fromAirportId) + .withToAirportId(toAirportId) + .build(); + + final AirportEntity fromAirportEntity = new AirportEntityBuilder() + .withId(fromAirportId) + .withName("Origin Airport") + .withCityName("Origin City") + .build(); + + final AirportEntity toAirportEntity = new AirportEntityBuilder() + .withId(toAirportId) + .withName("Destination Airport") + .withCityName("Destination City") + .build(); + + final FlightEntity existingFlightEntity = new FlightEntityBuilder() + .withId(flightId) + .withValidFields() + .build(); + + final FlightEntity updatedFlightEntity = new FlightEntityBuilder() + .withId(flightId) + .withFromAirport(fromAirportEntity) + .withToAirport(toAirportEntity) + .withDepartureTime(mockUpdateFlightRequest.getDepartureTime()) + .withArrivalTime(mockUpdateFlightRequest.getArrivalTime()) + .withPrice(mockUpdateFlightRequest.getPrice()) + .build(); + + final Flight expectedFlight = flightEntityToFlightMapper.map(updatedFlightEntity); + + // When + when(flightRepository.findById(flightId)).thenReturn(Optional.of(existingFlightEntity)); + when(airportRepository.findById(fromAirportId)).thenReturn(Optional.of(fromAirportEntity)); + when(airportRepository.findById(toAirportId)).thenReturn(Optional.of(toAirportEntity)); + when(flightRepository.save(any(FlightEntity.class))).thenReturn(updatedFlightEntity); + + // Then + Flight result = flightUpdateService.updateFlightById(flightId, mockUpdateFlightRequest); + + // Asserts for the expected values + assertNotNull(result); + assertEquals(expectedFlight.getFromAirport().getName(), result.getFromAirport().getName()); + assertEquals(expectedFlight.getFromAirport().getCityName(), result.getFromAirport().getCityName()); + assertEquals(expectedFlight.getToAirport().getName(), result.getToAirport().getName()); + assertEquals(expectedFlight.getToAirport().getCityName(), result.getToAirport().getCityName()); + assertEquals(expectedFlight.getDepartureTime(), result.getDepartureTime()); + assertEquals(expectedFlight.getArrivalTime(), result.getArrivalTime()); + assertEquals(expectedFlight.getPrice(), result.getPrice()); + + // Verify the mock interactions + verify(flightRepository, times(1)).findById(flightId); + verify(airportRepository, times(1)).findById(fromAirportId); + verify(airportRepository, times(1)).findById(toAirportId); + verify(flightRepository, times(1)).save(any(FlightEntity.class)); + + } + + + + @Test + void givenNonexistentFlightId_whenUpdateFlightById_thenThrowFlightNotFoundException() { + + // Given + final String flightId = UUID.randomUUID().toString(); + + final UpdateFlightRequest mockUpdateFlightRequest = new UpdateFlightRequestBuilder() + .withValidFields() + .build(); + + // When + when(flightRepository.findById(flightId)).thenReturn(Optional.empty()); + + // Then + assertThrows(FlightNotFoundException.class, () -> + flightUpdateService.updateFlightById(flightId, mockUpdateFlightRequest)); + + // Verify + verify(flightRepository, times(1)).findById(flightId); + verify(airportRepository, times(0)).findById(any()); + verify(flightRepository, times(0)).save(any()); + + } + + @Test + void givenNonexistentAirport_whenUpdateFlightById_thenThrowAirportNotFoundException() { + + // Given + final String flightId = UUID.randomUUID().toString(); + final String fromAirportId = UUID.randomUUID().toString(); + final String toAirportId = UUID.randomUUID().toString(); + + final UpdateFlightRequest mockUpdateFlightRequest = new UpdateFlightRequestBuilder() + .withValidFields() + .withFromAirportId(fromAirportId) + .withToAirportId(toAirportId) + .build(); + + final FlightEntity existingFlightEntity = new FlightEntityBuilder() + .withId(flightId) + .withValidFields() + .build(); + + // When + when(flightRepository.findById(flightId)).thenReturn(Optional.of(existingFlightEntity)); + when(airportRepository.findById(fromAirportId)).thenReturn(Optional.empty()); + when(airportRepository.findById(toAirportId)).thenReturn(Optional.of(new AirportEntity())); + + // Then + assertThrows(AirportNotFoundException.class, () -> + flightUpdateService.updateFlightById(flightId, mockUpdateFlightRequest)); + + // Verify + verify(flightRepository, times(1)).findById(flightId); + verify(airportRepository, times(1)).findById(fromAirportId); + verify(airportRepository, times(0)).findById(toAirportId); + verify(flightRepository, times(0)).save(any()); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/SearchFlightServiceImplTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/SearchFlightServiceImplTest.java new file mode 100644 index 000000000..d10b01960 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/service/flight/impl/SearchFlightServiceImplTest.java @@ -0,0 +1,223 @@ +package com.example.demo.flight.service.flight.impl; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.builder.AirportEntityBuilder; +import com.example.demo.builder.FlightEntityBuilder; +import com.example.demo.common.model.CustomPage; +import com.example.demo.common.model.CustomPaging; +import com.example.demo.flight.model.Flight; +import com.example.demo.flight.model.dto.request.flight.SearchFlightRequest; +import com.example.demo.flight.model.entity.FlightEntity; +import com.example.demo.flight.model.mapper.flight.ListFlightEntityToListFlightMapper; +import com.example.demo.flight.repository.SearchFlightRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit test class for {@link SearchFlightServiceImpl}. + * This class verifies the correctness of the business logic in {@link SearchFlightServiceImpl}. + */ +class SearchFlightServiceImplTest extends AbstractBaseServiceTest { + + @InjectMocks + private SearchFlightServiceImpl searchFlightService; + + @Mock + private SearchFlightRepository searchFlightRepository; + + private final ListFlightEntityToListFlightMapper listFlightEntityToListFlightMapper = + ListFlightEntityToListFlightMapper.initialize(); + + + @Test + void givenSearchFlightRequest_whenOneWayFlightsFound_thenReturnFlights() { + + // Given + final CustomPaging pagination = CustomPaging.builder() + .pageNumber(1) + .pageSize(10) + .build(); + + final SearchFlightRequest request = SearchFlightRequest.builder() + .fromAirportId(UUID.randomUUID().toString()) + .toAirportId(UUID.randomUUID().toString()) + .departureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .pagination(pagination) + .build(); + + final LocalDateTime departureStart = LocalDateTime.of(2025, 1, 19, 0, 0); + final LocalDateTime departureEnd = departureStart.plusDays(1).minusNanos(1); + + final FlightEntity flightEntity = new FlightEntityBuilder() + .withValidFields() + .withFromAirport(new AirportEntityBuilder() + .withId(request.getFromAirportId()) + .withValidFields()) + .withToAirport(new AirportEntityBuilder() + .withId(request.getToAirportId()) + .withValidFields()) + .withDepartureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .build(); + + final Page departureFlightEntities = new PageImpl<>(List.of(flightEntity)); + + final List flightList = listFlightEntityToListFlightMapper.toFlightList(departureFlightEntities.getContent()); + + // When + when(searchFlightRepository.findFlights( + request.getFromAirportId(), request.getToAirportId(), departureStart, departureEnd, request.toPageable())).thenReturn(departureFlightEntities); + + // Then + CustomPage result = searchFlightService.searchFlights(request); + + assertEquals(flightList.size(), result.getContent().size()); + + assertEquals(flightList.get(0).getFromAirport().getId(), result.getContent().get(0).getFromAirport().getId()); + assertEquals(flightList.get(0).getFromAirport().getName(), result.getContent().get(0).getFromAirport().getName()); + assertEquals(flightList.get(0).getFromAirport().getCityName(), result.getContent().get(0).getFromAirport().getCityName()); + assertEquals(flightList.get(0).getToAirport().getId(), result.getContent().get(0).getToAirport().getId()); + assertEquals(flightList.get(0).getToAirport().getName(), result.getContent().get(0).getToAirport().getName()); + assertEquals(flightList.get(0).getToAirport().getCityName(), result.getContent().get(0).getToAirport().getCityName()); + assertEquals(flightList.get(0).getPrice(), result.getContent().get(0).getPrice()); + + // Verify + verify(searchFlightRepository).findFlights(request.getFromAirportId(), request.getToAirportId(), + departureStart, departureEnd, request.toPageable()); + + } + + + @Test + void givenSearchFlightRequest_whenRoundTripFlightsFound_thenReturnFlights() { + + // Given + final CustomPaging pagination = CustomPaging.builder() + .pageNumber(1) + .pageSize(10) + .build(); + + final SearchFlightRequest request = SearchFlightRequest.builder() + .fromAirportId(UUID.randomUUID().toString()) + .toAirportId(UUID.randomUUID().toString()) + .departureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .arrivalTime(LocalDateTime.of(2025, 1, 20, 10, 0)) + .pagination(pagination) + .build(); + + final LocalDateTime departureStart = LocalDateTime.of(2025, 1, 19, 0, 0); + final LocalDateTime departureEnd = departureStart.plusDays(1).minusNanos(1); + + final LocalDateTime returnStart = LocalDateTime.of(2025, 1, 20, 0, 0); + final LocalDateTime returnEnd = returnStart.plusDays(1).minusNanos(1); + + final FlightEntity departureFlightEntity = new FlightEntityBuilder() + .withValidFields() + .withFromAirport(new AirportEntityBuilder() + .withId(request.getFromAirportId()) + .withValidFields()) + .withToAirport(new AirportEntityBuilder() + .withId(request.getToAirportId()) + .withValidFields()) + .withDepartureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .build(); + + final FlightEntity returnFlightEntity = new FlightEntityBuilder() + .withValidFields() + .withFromAirport(new AirportEntityBuilder() + .withId(request.getToAirportId()) + .withValidFields()) + .withToAirport(new AirportEntityBuilder() + .withId(request.getFromAirportId()) + .withValidFields()) + .withDepartureTime(LocalDateTime.of(2025, 1, 20, 10, 0)) + .build(); + + final Page departureFlightEntities = new PageImpl<>(List.of(departureFlightEntity)); + final Page returnFlightEntities = new PageImpl<>(List.of(returnFlightEntity)); + + final List departureFlights = listFlightEntityToListFlightMapper.toFlightList(departureFlightEntities.getContent()); + final List returnFlights = listFlightEntityToListFlightMapper.toFlightList(returnFlightEntities.getContent()); + + // When + when(searchFlightRepository.findFlights( + request.getFromAirportId(), request.getToAirportId(), departureStart, departureEnd, request.toPageable())).thenReturn(departureFlightEntities); + when(searchFlightRepository.findFlights( + request.getToAirportId(), request.getFromAirportId(), returnStart, returnEnd, request.toPageable())).thenReturn(returnFlightEntities); + + // Then + CustomPage result = searchFlightService.searchFlights(request); + + assertNotNull(result, "Result should not be null"); + assertEquals(departureFlights.size() + returnFlights.size(), result.getContent().size(), "Total flights should match the sum of departure and return flights"); + + assertEquals(departureFlights.get(0).getFromAirport().getId(), + result.getContent().get(0).getFromAirport().getId(), "Departure flight from airport ID should match"); + assertEquals(departureFlights.get(0).getToAirport().getId(), + result.getContent().get(0).getToAirport().getId(), "Departure flight to airport ID should match"); + assertEquals(returnFlights.get(0).getFromAirport().getId(), + result.getContent().get(1).getFromAirport().getId(), "Return flight from airport ID should match"); + assertEquals(returnFlights.get(0).getToAirport().getId(), + result.getContent().get(1).getToAirport().getId(), "Return flight to airport ID should match"); + + // Verify + verify(searchFlightRepository).findFlights(request.getFromAirportId(), request.getToAirportId(), + departureStart, departureEnd, request.toPageable()); + verify(searchFlightRepository).findFlights(request.getToAirportId(), request.getFromAirportId(), + returnStart, returnEnd, request.toPageable()); + + } + + @Test + void givenSearchFlightRequest_whenNoFlightFound_thenReturnEmptyList() { + + // Given + final CustomPaging pagination = CustomPaging.builder() + .pageNumber(1) + .pageSize(10) + .build(); + + final SearchFlightRequest request = SearchFlightRequest.builder() + .fromAirportId(UUID.randomUUID().toString()) + .toAirportId(UUID.randomUUID().toString()) + .departureTime(LocalDateTime.of(2025, 1, 19, 10, 0)) + .pagination(pagination) + .build(); + + final LocalDateTime departureStart = LocalDateTime.of(2025, 1, 19, 0, 0); + final LocalDateTime departureEnd = departureStart.plusDays(1).minusNanos(1); + + final Page emptyPage = new PageImpl<>(Collections.emptyList()); + final List emptyFlightList = listFlightEntityToListFlightMapper.toFlightList(emptyPage.getContent()); + + // When + when(searchFlightRepository.findFlights( + request.getFromAirportId(), request.getToAirportId(), departureStart, departureEnd, request.toPageable())).thenReturn(emptyPage); + + CustomPage result = searchFlightService.searchFlights(request); + + // Then + assertNotNull(result, "Result should not be null"); + assertNotNull(emptyFlightList, "Mapped flight list should not be null"); + assertTrue(emptyFlightList.isEmpty(), "Mapped flight list should be empty"); + assertEquals(emptyFlightList.size(), result.getContent().size(), "Result content size should match empty flight list size"); + assertTrue(result.getContent().isEmpty(), "Result content should be empty"); + + // Verify + verify(searchFlightRepository).findFlights(request.getFromAirportId(), request.getToAirportId(), departureStart, departureEnd, request.toPageable()); + + } + + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/AirportUtilityClassTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/AirportUtilityClassTest.java new file mode 100644 index 000000000..1c29b1100 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/AirportUtilityClassTest.java @@ -0,0 +1,75 @@ +package com.example.demo.flight.utils; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.repository.AirportRepository; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.lang.reflect.InvocationTargetException; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit test class for {@link AirportUtilityClass}. + * This class tests the utility methods of {@link AirportUtilityClass}, ensuring + * they function as expected. It also verifies that the utility class cannot be + * instantiated, as it is intended to be a utility class. + */ +class AirportUtilityClassTest extends AbstractBaseServiceTest { + + @Mock + private AirportRepository airportRepository; + + @Test + void utilityClass_ShouldNotBeInstantiated() { + assertThrows(InvocationTargetException.class, () -> { + // Attempt to use reflection to create an instance of the utility class + java.lang.reflect.Constructor constructor = AirportUtilityClass.class.getDeclaredConstructor(); + constructor.setAccessible(true); + constructor.newInstance(); + }); + } + + @Test + void checkAirportNameUniqueness_ShouldThrowException_WhenNameExists() { + + // Given + String existingAirportName = "Existing Airport"; + + // When + when(airportRepository.existsByName(existingAirportName)).thenReturn(true); + + // Then + assertThrows(AirportNameAlreadyExistException.class, () -> + AirportUtilityClass.checkAirportNameUniqueness(airportRepository, existingAirportName) + ); + + // Verify + verify(airportRepository).existsByName(existingAirportName); + + } + + @Test + void checkAirportNameUniqueness_ShouldNotThrowException_WhenNameDoesNotExist() { + + // Given + String newAirportName = "New Airport"; + + // When + when(airportRepository.existsByName(newAirportName)).thenReturn(false); + + // Then + assertDoesNotThrow(() -> + AirportUtilityClass.checkAirportNameUniqueness(airportRepository, newAirportName) + ); + + // Verify + verify(airportRepository).existsByName(newAirportName); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/CustomLocalDateTimeDeserializerTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/CustomLocalDateTimeDeserializerTest.java new file mode 100644 index 000000000..79cca6edd --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/CustomLocalDateTimeDeserializerTest.java @@ -0,0 +1,79 @@ +package com.example.demo.flight.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test class for {@link CustomLocalDateTimeDeserializer}. + * This class is responsible for testing the functionality of the custom deserializer + * for {@link LocalDateTime}. It uses {@link ObjectMapper} configured with the + * {@link CustomLocalDateTimeDeserializer} to verify the deserialization logic. + */ +class CustomLocalDateTimeDeserializerTest { + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + // Set up ObjectMapper with the custom deserializer + objectMapper = new ObjectMapper(); + SimpleModule module = new SimpleModule(); + module.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer()); + objectMapper.registerModule(module); + } + + @Test + void testDeserializeValidDate() throws IOException { + // Given a date string + String dateString = "\"2025-01-20\""; // Representing "2025-01-20T00:00:00" + + // When deserialized + LocalDateTime result = objectMapper.readValue(dateString, LocalDateTime.class); + + // Then the result should be a LocalDateTime at midnight (start of the day) + assertNotNull(result); + assertEquals(LocalDateTime.of(2025, 1, 20, 0, 0, 0, 0), result); + } + + @Test + void testDeserializeEmptyDate() throws IOException { + // Given an empty date string + String dateString = "\"\""; + + // When deserialized + LocalDateTime result = objectMapper.readValue(dateString, LocalDateTime.class); + + // Then the result should be null + assertNull(result); + } + + @Test + void testDeserializeNullDate() throws IOException { + // Given a null date string + String dateString = "null"; + + // When deserialized + LocalDateTime result = objectMapper.readValue(dateString, LocalDateTime.class); + + // Then the result should be null + assertNull(result); + } + + @Test + void testDeserializeInvalidDate() { + // Given an invalid date string + String dateString = "\"invalid-date\""; + + // When deserialized + assertThrows(DateTimeParseException.class, () -> objectMapper.readValue(dateString, LocalDateTime.class)); + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/validator/ArrivalTimeValidatorTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/validator/ArrivalTimeValidatorTest.java new file mode 100644 index 000000000..61d43f064 --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/flight/utils/validator/ArrivalTimeValidatorTest.java @@ -0,0 +1,232 @@ +package com.example.demo.flight.utils.validator; + +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.flight.model.dto.request.flight.CreateFlightRequest; +import com.example.demo.flight.model.dto.request.flight.UpdateFlightRequest; +import jakarta.validation.ConstraintValidatorContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit test class for {@link ArrivalTimeValidator}. + * This test class is responsible for testing the validation logic of the {@link ArrivalTimeValidator}. + * It extends {@link AbstractBaseServiceTest} and uses {@link Mock} annotations to mock the necessary dependencies. + */ +class ArrivalTimeValidatorTest extends AbstractBaseServiceTest { + + private ArrivalTimeValidator arrivalTimeValidator; + + @Mock + private ConstraintValidatorContext context; + + @BeforeEach + void setUp() { + arrivalTimeValidator = new ArrivalTimeValidator(); + } + + @Test + void testValidArrivalTimeAfterDepartureTime() { + // Given + CreateFlightRequest validRequest = new CreateFlightRequest(); + validRequest.setDepartureTime(LocalDateTime.of(2025, 1, 13, 10, 0)); + validRequest.setArrivalTime(LocalDateTime.of(2025, 1, 13, 12, 0)); + + // When + boolean isValid = arrivalTimeValidator.isValid(validRequest, context); + + // Then + assertTrue(isValid, "Arrival time should be after departure time."); + } + + @Test + void testInvalidArrivalTimeBeforeDepartureTime() { + // Given + CreateFlightRequest invalidRequest = new CreateFlightRequest(); + invalidRequest.setDepartureTime(LocalDateTime.of(2025, 1, 13, 10, 0)); + invalidRequest.setArrivalTime(LocalDateTime.of(2025, 1, 13, 8, 0)); + + // When + boolean isValid = arrivalTimeValidator.isValid(invalidRequest, context); + + // Then + assertFalse(isValid, "Arrival time cannot be earlier than departure time."); + } + + @Test + void testNullArrivalTime() { + // Given + CreateFlightRequest requestWithNullArrival = new CreateFlightRequest(); + requestWithNullArrival.setDepartureTime(LocalDateTime.of(2025, 1, 13, 10, 0)); + requestWithNullArrival.setArrivalTime(null); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithNullArrival, context); + + // Then + assertTrue(isValid, "Arrival time should be valid when it is null."); + } + + @Test + void testNullDepartureTime() { + // Given + CreateFlightRequest requestWithNullDeparture = new CreateFlightRequest(); + requestWithNullDeparture.setDepartureTime(null); + requestWithNullDeparture.setArrivalTime(LocalDateTime.of(2025, 1, 13, 12, 0)); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithNullDeparture, context); + + // Then + assertTrue(isValid, "Arrival time should be valid when departure time is null."); + } + + @Test + void testBothTimesNull() { + // Given + CreateFlightRequest requestWithBothNull = new CreateFlightRequest(); + requestWithBothNull.setDepartureTime(null); + requestWithBothNull.setArrivalTime(null); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithBothNull, context); + + // Then + assertTrue(isValid, "Both arrival and departure times should be valid when null."); + } + + @Test + void testArrivalTimeEqualToDepartureTime() { + // Given + CreateFlightRequest requestWithEqualTimes = new CreateFlightRequest(); + LocalDateTime sameTime = LocalDateTime.of(2025, 1, 13, 10, 0); + requestWithEqualTimes.setDepartureTime(sameTime); + requestWithEqualTimes.setArrivalTime(sameTime); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithEqualTimes, context); + + // Then + assertTrue(isValid, "Arrival time should be valid if it is equal to departure time."); + } + + @Test + void testValidUpdateArrivalTimeAfterDepartureTime() { + // Given + UpdateFlightRequest validRequest = new UpdateFlightRequest(); + validRequest.setDepartureTime(LocalDateTime.of(2025, 1, 13, 10, 0)); + validRequest.setArrivalTime(LocalDateTime.of(2025, 1, 13, 12, 0)); + + // When + boolean isValid = arrivalTimeValidator.isValid(validRequest, context); + + // Then + assertTrue(isValid, "Arrival time should be after departure time."); + } + + @Test + void testInvalidUpdateArrivalTimeBeforeDepartureTime() { + // Given + UpdateFlightRequest invalidRequest = new UpdateFlightRequest(); + invalidRequest.setDepartureTime(LocalDateTime.of(2025, 1, 13, 10, 0)); + invalidRequest.setArrivalTime(LocalDateTime.of(2025, 1, 13, 8, 0)); + + // When + boolean isValid = arrivalTimeValidator.isValid(invalidRequest, context); + + // Then + assertFalse(isValid, "Arrival time cannot be earlier than departure time."); + } + + @Test + void testNullUpdateArrivalTime() { + // Given + UpdateFlightRequest requestWithNullArrival = new UpdateFlightRequest(); + requestWithNullArrival.setDepartureTime(LocalDateTime.of(2025, 1, 13, 10, 0)); + requestWithNullArrival.setArrivalTime(null); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithNullArrival, context); + + // Then + assertTrue(isValid, "Arrival time should be valid when it is null."); + } + + @Test + void testNullUpdateDepartureTime() { + // Given + UpdateFlightRequest requestWithNullDeparture = new UpdateFlightRequest(); + requestWithNullDeparture.setDepartureTime(null); + requestWithNullDeparture.setArrivalTime(LocalDateTime.of(2025, 1, 13, 12, 0)); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithNullDeparture, context); + + // Then + assertTrue(isValid, "Arrival time should be valid when departure time is null."); + } + + @Test + void testBothUpdateTimesNull() { + // Given + UpdateFlightRequest requestWithBothNull = new UpdateFlightRequest(); + requestWithBothNull.setDepartureTime(null); + requestWithBothNull.setArrivalTime(null); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithBothNull, context); + + // Then + assertTrue(isValid, "Both arrival and departure times should be valid when null."); + } + + @Test + void testUpdateArrivalTimeEqualToDepartureTime() { + // Given + UpdateFlightRequest requestWithEqualTimes = new UpdateFlightRequest(); + LocalDateTime sameTime = LocalDateTime.of(2025, 1, 13, 10, 0); + requestWithEqualTimes.setDepartureTime(sameTime); + requestWithEqualTimes.setArrivalTime(sameTime); + + // When + boolean isValid = arrivalTimeValidator.isValid(requestWithEqualTimes, context); + + // Then + assertTrue(isValid, "Arrival time should be valid if it is equal to departure time."); + } + + @Test + void testNullValue() { + + // Given + ArrivalTimeValidator arrivalTimeValidator = new ArrivalTimeValidator(); + + // When + boolean isValid = arrivalTimeValidator.isValid(null, context); + + // Then + assertTrue(isValid, "Validation should pass when the value is null."); + + } + + @Test + void testUnrecognizedObjectType() { + + // Given + ArrivalTimeValidator arrivalTimeValidator = new ArrivalTimeValidator(); + Object unrecognizedObject = new Object(); // Some other object type not recognized by the validator + + // When + boolean isValid = arrivalTimeValidator.isValid(unrecognizedObject, context); + + // Then + assertTrue(isValid, "Validation should pass when the object type is not recognized."); + + } + +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/logging/aop/LoggerAspectJTest.java b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/logging/aop/LoggerAspectJTest.java new file mode 100644 index 000000000..f060b3ebf --- /dev/null +++ b/jdk_21_maven/cs/rest/flight-search-api/src/test/java/com/example/demo/logging/aop/LoggerAspectJTest.java @@ -0,0 +1,256 @@ +package com.example.demo.logging.aop; + +import com.example.demo.auth.exception.*; +import com.example.demo.base.AbstractBaseServiceTest; +import com.example.demo.flight.exception.AirportNameAlreadyExistException; +import com.example.demo.flight.exception.AirportNotFoundException; +import com.example.demo.flight.exception.FlightNotFoundException; +import com.example.demo.logging.entity.LogEntity; +import com.example.demo.logging.service.LogService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.Signature; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Test class for the {@link LoggerAspectJ}, verifying that logging aspects + * are correctly applied to target methods and provide the expected output. + */ +class LoggerAspectJTest extends AbstractBaseServiceTest { + + @InjectMocks + private LoggerAspectJ loggerAspectJ; + + @Mock + private LogService logService; + + @Mock + private HttpServletRequest httpServletRequest; + + @Mock + private HttpServletResponse httpServletResponse; + + @Mock + private ServletRequestAttributes servletRequestAttributes; + + @Mock + private Authentication authentication; + + @Mock + private SecurityContext securityContext; + + @Mock + private JoinPoint joinPoint; + + @Mock + private Signature signature; + + @BeforeEach + public void setUp() { + // Initialize mocks and set request attributes + when(servletRequestAttributes.getRequest()).thenReturn(httpServletRequest); + when(servletRequestAttributes.getResponse()).thenReturn(httpServletResponse); + RequestContextHolder.setRequestAttributes(servletRequestAttributes); + + // Mock JoinPoint signature + when(joinPoint.getSignature()).thenReturn(signature); + when(signature.getName()).thenReturn("testMethod"); + when(signature.getDeclaringTypeName()).thenReturn("LoggerAspectJ"); + when(signature.getDeclaringType()).thenReturn(LoggerAspectJ.class); + } + + @Test + public void testLogAfterThrowing() { + + // Given + Exception ex = new UserNotFoundException("User not found"); + + // When + when(httpServletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/api/test")); + when(httpServletRequest.getMethod()).thenReturn("GET"); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn("testUser"); + SecurityContextHolder.setContext(securityContext); + + // Then + loggerAspectJ.logAfterThrowing(joinPoint, ex); + + // Verify + verify(logService, times(1)).saveLogToDatabase(any(LogEntity.class)); + + } + + @Test + public void testLogAfterThrowing_AnonymousUser() { + + // Given + Exception ex = new UserNotFoundException("User not found"); + GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ANONYMOUS"); + List authorities = Collections.singletonList(authority); + SecurityContextHolder.setContext(securityContext); + + // When + when(httpServletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/api/test")); + when(httpServletRequest.getMethod()).thenReturn("GET"); + when(securityContext.getAuthentication()).thenReturn(new AnonymousAuthenticationToken("key", "anonymousUser", authorities)); + + // Then + loggerAspectJ.logAfterThrowing(joinPoint, ex); + + // Verify + verify(logService, times(1)).saveLogToDatabase(any(LogEntity.class)); + + } + + @Test + public void testLogAfterReturning() throws IOException { + + // When + when(httpServletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/api/test")); + when(httpServletRequest.getMethod()).thenReturn("POST"); + when(httpServletResponse.getStatus()).thenReturn(HttpStatus.OK.value()); + when(signature.getName()).thenReturn("testMethod"); + when(joinPoint.getSignature()).thenReturn(signature); + + // Then + loggerAspectJ.logAfterReturning(joinPoint, "test response"); + + // Verify + verify(logService, times(1)).saveLogToDatabase(any(LogEntity.class)); + + } + + @Test + public void testLogAfterReturning_WithJsonNode() throws IOException { + + // Given + JsonNode jsonNode = new ObjectMapper().createObjectNode().put("key", "value"); + + // When + when(httpServletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/api/test")); + when(httpServletRequest.getMethod()).thenReturn("POST"); + when(httpServletResponse.getStatus()).thenReturn(HttpStatus.OK.value()); + + // Then + loggerAspectJ.logAfterReturning(joinPoint, jsonNode); + + // Verify + verify(logService, times(1)).saveLogToDatabase(any(LogEntity.class)); + + } + + @Test + public void testLogAfterReturning_NoResponseAttributes() throws IOException { + + // Given + RequestContextHolder.resetRequestAttributes(); + + // When + loggerAspectJ.logAfterReturning(mock(JoinPoint.class), "test response"); + + // Then + verify(logService, never()).saveLogToDatabase(any(LogEntity.class)); + + } + + @Test + public void testLogAfterThrowing_LogServiceException() { + + // Given + Exception ex = new UserNotFoundException("User not found"); + + // When + when(httpServletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/api/test")); + when(httpServletRequest.getMethod()).thenReturn("GET"); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn("testUser"); + SecurityContextHolder.setContext(securityContext); + doThrow(new RuntimeException("Database connection error")).when(logService).saveLogToDatabase(any(LogEntity.class)); + + // Then + loggerAspectJ.logAfterThrowing(joinPoint, ex); + + // Verify + verify(logService, times(1)).saveLogToDatabase(any(LogEntity.class)); + + } + + @Test + public void testLogAfterReturning_SaveLogThrowsException() throws IOException { + + // Given + when(httpServletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/api/test")); + when(httpServletRequest.getMethod()).thenReturn("POST"); + when(httpServletResponse.getStatus()).thenReturn(HttpStatus.OK.value()); + when(signature.getName()).thenReturn("testMethod"); + when(joinPoint.getSignature()).thenReturn(signature); + + // When + doThrow(new RuntimeException("Database error")).when(logService).saveLogToDatabase(any(LogEntity.class)); + + // Then + Assertions.assertDoesNotThrow(() -> loggerAspectJ.logAfterReturning(joinPoint, "test response")); + + // Verify + verify(logService, times(1)).saveLogToDatabase(any(LogEntity.class)); + + } + + @Test + public void testGetHttpStatusFromException_AllCases() { + + // Given + Map testCases = Map.of( + new PasswordNotValidException("Invalid password"), PasswordNotValidException.STATUS.name(), + new RoleNotFoundException("Role not found"), RoleNotFoundException.STATUS.name(), + new TokenAlreadyInvalidatedException("Token already invalidated"), TokenAlreadyInvalidatedException.STATUS.name(), + new UserAlreadyExistException("User already exists"), UserAlreadyExistException.STATUS.name(), + new UserNotFoundException("User not found"), UserNotFoundException.STATUS.name(), + new UserStatusNotValidException("User status not valid"), UserStatusNotValidException.STATUS.name(), + new AirportNotFoundException("Airport not found"), AirportNotFoundException.STATUS.name(), + new AirportNameAlreadyExistException("Airport name is already exist"), AirportNameAlreadyExistException.STATUS.name(), + new FlightNotFoundException("Airport not found"), FlightNotFoundException.STATUS.name(), + new Exception("Unknown exception"), HttpStatus.INTERNAL_SERVER_ERROR.name() + ); + + // When & Then + testCases.forEach((exception, expectedStatus) -> { + String actualStatus = (String) ReflectionTestUtils.invokeMethod( + loggerAspectJ, + "getHttpStatusFromException", + exception + ); + Assertions.assertEquals(expectedStatus, actualStatus, + "Failed for exception: " + exception.getClass().getSimpleName()); + }); + + } + +} \ No newline at end of file