From 8cc18732da248e209b45c6ecb07df284edae59e5 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Thu, 18 Jun 2026 10:03:09 +0200 Subject: [PATCH 1/7] add api description --- api/openapi.yaml | 156 ++++++++++- services/py-genai-helper/generated/models.py | 5 +- .../devoops/eventservice/api/EventsApi.java | 10 + .../feedbackservice/api/FeedbackApi.java | 10 + .../financeservice/api/FinanceApi.java | 14 + .../devoops/letterservice/api/LettersApi.java | 4 + .../devoops/memberservice/api/MembersApi.java | 10 + .../memberservice/model/MemberCreate.java | 29 +- .../api/OrganizationApi.java | 24 +- web-client/src/api.ts | 251 +++++++++++++++--- 10 files changed, 468 insertions(+), 45 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 92163ec..62d3c70 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -15,7 +15,9 @@ paths: tags: - organization summary: Get all sports - description: Returns a list of all sports registered in the organization. If the caller is not an admin, only sports that the caller is a member of will be returned. + description: | + Returns a list of all sports registered in the organization. + - All authenticated users: can access this endpoint. responses: "200": description: The request was successful, and the server has returned the requested resource in the response body. @@ -39,7 +41,9 @@ paths: tags: - organization summary: Create sport - description: Creates a new sport in the organization. Only admins are allowed to create new sports. + description: | + Creates a new sport in the organization. + - Admins: can create sports. responses: "201": description: The request was successful, and a new resource was created. @@ -72,6 +76,10 @@ paths: tags: - organization summary: Update sport + description: | + Partially updates an existing sport's details. + - Directors: can update all fields except directors. + - Admins: can update all fields. parameters: - $ref: "#/components/parameters/sport_name" responses: @@ -106,6 +114,9 @@ paths: tags: - organization summary: Delete sport + description: | + Deletes a sport from the organization. + - Admins: can delete sports. parameters: - $ref: "#/components/parameters/sport_name" responses: @@ -127,6 +138,9 @@ paths: tags: - organization summary: Get sport + description: | + Returns the details of a specific sport. + - All authenticated users: can access this endpoint. parameters: - $ref: "#/components/parameters/sport_name" responses: @@ -153,6 +167,9 @@ paths: tags: - organization summary: Get all teams + description: | + Returns a list of all teams in the organization. + - All authenticated users: can access this endpoint. responses: "200": description: The request was successful, and the server has returned the @@ -176,6 +193,10 @@ paths: tags: - organization summary: Create team + description: | + Creates a new team in the organization. + - Directors: can create teams for their own sport. + - Admins: can create teams for any sport. responses: "201": description: The request was successful, and a new resource was created. @@ -208,6 +229,9 @@ paths: tags: - organization summary: Get team + description: | + Returns the details of a specific team. + - All authenticated users: can access this endpoint. parameters: - $ref: "#/components/parameters/team_id" responses: @@ -233,6 +257,11 @@ paths: tags: - organization summary: Update team + description: | + Partially updates a team's details. + - Trainers: can update all fields except sport and trainers. + - Directors: can update all fields except sport. + - Admins: can update all fields. parameters: - $ref: "#/components/parameters/team_id" responses: @@ -267,6 +296,10 @@ paths: tags: - organization summary: Delete team + description: | + Deletes a team from the organization. + - Directors: can delete teams in their sport. + - Admins: can delete any team. parameters: - $ref: "#/components/parameters/team_id" responses: @@ -289,6 +322,9 @@ paths: tags: - members summary: Get all members + description: | + Returns a list of all members in the organization. + - All authenticated users: can access this endpoint. responses: "200": description: The request was successful, and the server has returned the @@ -312,6 +348,9 @@ paths: tags: - members summary: Create member + description: | + Creates a new member in the organization. Includes a password field for setting initial credentials. + - Admins: can create members. responses: "201": description: The request was successful, and a new resource was created. @@ -344,6 +383,13 @@ paths: tags: - members summary: Get member details + description: | + Returns the full details of a specific member. + - Members themselves: can view their own details. + - Team members: can view details of others in the same team. + - Trainers: can view details of members in their team. + - Directors: can view details of members in their sport. + - Admins: can view any member's details. parameters: - $ref: "#/components/parameters/member_id" responses: @@ -369,6 +415,10 @@ paths: tags: - members summary: Update member details + description: | + Partially updates the details of a specific member. + - Members themselves: can update their own details. + - Admins: can update any member's details. parameters: - $ref: "#/components/parameters/member_id" responses: @@ -403,6 +453,9 @@ paths: tags: - members summary: Delete member + description: | + Deletes a member from the organization. + - Admins: can delete members. parameters: - $ref: "#/components/parameters/member_id" responses: @@ -425,6 +478,11 @@ paths: tags: - events summary: Get all events + description: | + Returns a list of all events. + - All authenticated users: can see events linked to their team or sport, or events where they are an attendee. + - Creators: can see all events they created. + - Admins: can see all events. responses: "200": description: The request was successful, and the server has returned the @@ -448,6 +506,11 @@ paths: tags: - events summary: Create event + description: | + Creates a new event. + - Directors: can create events for their sport. + - Trainers: can create events for their team. + - Admins: can create any event. responses: "201": description: The request was successful, and a new resource was created. @@ -480,6 +543,11 @@ paths: tags: - events summary: Get event details + description: | + Returns the details of a specific event. + - All authenticated users: can access events linked to their team or sport, or events where they are an attendee. + - Creators: can view events they created. + - Admins: can view any event. parameters: - $ref: "#/components/parameters/event_id" responses: @@ -505,6 +573,11 @@ paths: tags: - events summary: Update event details + description: | + Partially updates the details of a specific event. + - Creators: can update events they created. + - Directors: can update events linked to their sport. + - Admins: can update any event. parameters: - $ref: "#/components/parameters/event_id" responses: @@ -539,6 +612,11 @@ paths: tags: - events summary: Delete event + description: | + Deletes a specific event. + - Creators: can delete events they created. + - Directors: can delete events linked to their sport. + - Admins: can delete any event. parameters: - $ref: "#/components/parameters/event_id" responses: @@ -561,6 +639,11 @@ paths: tags: - feedback summary: Get all feedback + description: | + Returns a list of all feedback entries. + - Creators: can see feedback they submitted. + - Members: can see feedback about themselves. + - Admins: can see all feedback. responses: "200": description: The request was successful, and the server has returned the @@ -584,6 +667,10 @@ paths: tags: - feedback summary: Create feedback + description: | + Creates a new feedback entry for a member. + - Trainers: can create feedback for their trainees. + - Admins: can create feedback for any member. responses: "201": description: The request was successful, and a new resource was created. @@ -616,6 +703,11 @@ paths: tags: - feedback summary: Get feedback details + description: | + Returns the details of a specific feedback entry. + - Creators: can view feedback they submitted. + - Members: can view feedback about themselves. + - Admins: can view any feedback. parameters: - $ref: "#/components/parameters/feedback_id" responses: @@ -641,6 +733,10 @@ paths: tags: - feedback summary: Delete feedback + description: | + Deletes a specific feedback entry. + - Creators: can delete feedback they submitted. + - Admins: can delete any feedback. parameters: - $ref: "#/components/parameters/feedback_id" responses: @@ -662,6 +758,10 @@ paths: tags: - feedback summary: Update feedback details + description: | + Partially updates a specific feedback entry. + - Creators: can update feedback they submitted. + - Admins: can update any feedback. parameters: - $ref: "#/components/parameters/feedback_id" responses: @@ -697,6 +797,10 @@ paths: tags: - finance summary: Get all balances + description: | + Returns a list of all member balances. + - Directors: can view balances of members in their sport. + - Admins: can view all balances. responses: "200": description: The request was successful, and the server has returned the @@ -721,6 +825,11 @@ paths: tags: - finance summary: Get member balance + description: | + Returns the balance of a specific member. + - Members themselves: can view their own balance. + - Directors: can view balances of members in their sport. + - Admins: can view any member's balance. parameters: - $ref: "#/components/parameters/member_id" responses: @@ -747,6 +856,12 @@ paths: tags: - finance summary: Get all transactions + description: | + Returns a list of all transactions. Users only see transactions where they are the member or the creator. + - Members: can see transactions they are part of. + - Creators: can see transactions they created. + - Directors: can see transactions for members in their sport. + - Admins: can see all transactions. responses: "200": description: The request was successful, and the server has returned the @@ -770,6 +885,10 @@ paths: tags: - finance summary: Create transaction + description: | + Creates a new financial transaction for a member. + - Directors: can create transactions for members in their sport. + - Admins: can create transactions for any member. responses: "201": description: The request was successful, and a new resource was created. @@ -802,6 +921,12 @@ paths: tags: - finance summary: Get transaction + description: | + Returns the details of a specific transaction. + - Members: can view transactions they are part of. + - Creators: can view transactions they created. + - Directors: can view transactions for members in their sport. + - Admins: can view any transaction. parameters: - $ref: "#/components/parameters/transaction_id" responses: @@ -827,6 +952,10 @@ paths: tags: - finance summary: Update transaction + description: | + Partially updates a specific transaction. The member field can only be changed by admins. + - Creators: can update transactions they created (except the member field). + - Admins: can update any transaction including the member field. parameters: - $ref: "#/components/parameters/transaction_id" responses: @@ -861,6 +990,10 @@ paths: tags: - finance summary: Delete transaction + description: | + Deletes a specific transaction. + - Creators: can delete transactions they created. + - Admins: can delete any transaction. parameters: - $ref: "#/components/parameters/transaction_id" responses: @@ -883,6 +1016,11 @@ paths: tags: - letters summary: Send mail + description: | + Sends an email based on the provided HTML template. + - Trainers: can send mail to members of their team. + - Directors: can send mail to members related to their sport. + - Admins: can send mail to any member. responses: "204": $ref: "#/components/responses/NoContent" @@ -910,6 +1048,11 @@ paths: tags: - letters summary: Get pdf + description: | + Generates and returns a PDF document from the provided HTML template. + - Trainers: can generate PDFs related to their team. + - Directors: can generate PDFs related to their sport. + - Admins: can generate PDFs related to any member. responses: "200": description: The request was successful, and the server has returned the @@ -943,6 +1086,11 @@ paths: tags: - helper summary: Generate report + description: | + Generates an AI-based report for a member. Members can only generate reports for themselves. + - All authenticated users: can generate a report for themselves. + - Trainers: can generate reports for members of their team. + - Admin: can generate a report for any member. responses: "200": description: The request was successful, and the server has returned the @@ -1284,6 +1432,9 @@ components: type: string email: type: string + password: + type: string + format: password birthday: type: string format: date @@ -1297,6 +1448,7 @@ components: - first_name - last_name - email + - password description: Data transfer object for creating a new Member. Event: type: object diff --git a/services/py-genai-helper/generated/models.py b/services/py-genai-helper/generated/models.py index 5b07de0..65b8b3b 100644 --- a/services/py-genai-helper/generated/models.py +++ b/services/py-genai-helper/generated/models.py @@ -1,9 +1,9 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-06-12T12:58:15+00:00 +# timestamp: 2026-06-18T08:03:34+00:00 from __future__ import annotations -from pydantic import AwareDatetime, BaseModel, Field +from pydantic import AwareDatetime, BaseModel, Field, SecretStr from datetime import date from uuid import UUID from typing import Annotated @@ -99,6 +99,7 @@ class MemberCreate(BaseModel): first_name: str last_name: str email: str + password: SecretStr birthday: date | None = None phone_number: str | None = None address: str | None = None diff --git a/services/spring-event/src/generated/java/tum/devoops/eventservice/api/EventsApi.java b/services/spring-event/src/generated/java/tum/devoops/eventservice/api/EventsApi.java index ed7d69c..8567f1f 100644 --- a/services/spring-event/src/generated/java/tum/devoops/eventservice/api/EventsApi.java +++ b/services/spring-event/src/generated/java/tum/devoops/eventservice/api/EventsApi.java @@ -51,6 +51,7 @@ default Optional getRequest() { /** * POST /events : Create event + * Creates a new event. - Directors: can create events for their sport. - Trainers: can create events for their team. - Admins: can create any event. * * @param eventCreate The request body for creating a new event. (required) * @return The request was successful, and a new resource was created. (status code 201) @@ -63,6 +64,7 @@ default Optional getRequest() { @Operation( operationId = "createEvent", summary = "Create event", + description = "Creates a new event. - Directors: can create events for their sport. - Trainers: can create events for their team. - Admins: can create any event. ", tags = { "events" }, responses = { @ApiResponse(responseCode = "201", description = "The request was successful, and a new resource was created.", content = { @@ -139,6 +141,7 @@ default ResponseEntity createEvent( /** * DELETE /events/{event_id} : Delete event + * Deletes a specific event. - Creators: can delete events they created. - Directors: can delete events linked to their sport. - Admins: can delete any event. * * @param eventId (required) * @return The request was successful, but there is no content to return in the response. (status code 204) @@ -150,6 +153,7 @@ default ResponseEntity createEvent( @Operation( operationId = "deleteEvent", summary = "Delete event", + description = "Deletes a specific event. - Creators: can delete events they created. - Directors: can delete events linked to their sport. - Admins: can delete any event. ", tags = { "events" }, responses = { @ApiResponse(responseCode = "204", description = "The request was successful, but there is no content to return in the response."), @@ -210,6 +214,7 @@ default ResponseEntity deleteEvent( /** * GET /events : Get all events + * Returns a list of all events. - All authenticated users: can see events linked to their team or sport, or events where they are an attendee. - Creators: can see all events they created. - Admins: can see all events. * * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) * or Authentication is required to access the requested resource. The client must include the appropriate credentials. (status code 401) @@ -219,6 +224,7 @@ default ResponseEntity deleteEvent( @Operation( operationId = "getAllEvents", summary = "Get all events", + description = "Returns a list of all events. - All authenticated users: can see events linked to their team or sport, or events where they are an attendee. - Creators: can see all events they created. - Admins: can see all events. ", tags = { "events" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -278,6 +284,7 @@ default ResponseEntity> getAllEvents( /** * GET /events/{event_id} : Get event details + * Returns the details of a specific event. - All authenticated users: can access events linked to their team or sport, or events where they are an attendee. - Creators: can view events they created. - Admins: can view any event. * * @param eventId (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -289,6 +296,7 @@ default ResponseEntity> getAllEvents( @Operation( operationId = "getEventDetails", summary = "Get event details", + description = "Returns the details of a specific event. - All authenticated users: can access events linked to their team or sport, or events where they are an attendee. - Creators: can view events they created. - Admins: can view any event. ", tags = { "events" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -356,6 +364,7 @@ default ResponseEntity getEventDetails( /** * PATCH /events/{event_id} : Update event details + * Partially updates the details of a specific event. - Creators: can update events they created. - Directors: can update events linked to their sport. - Admins: can update any event. * * @param eventId (required) * @param eventPartialUpdate The request body for partially updating an event. (required) @@ -369,6 +378,7 @@ default ResponseEntity getEventDetails( @Operation( operationId = "updateEventDetails", summary = "Update event details", + description = "Partially updates the details of a specific event. - Creators: can update events they created. - Directors: can update events linked to their sport. - Admins: can update any event. ", tags = { "events" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { diff --git a/services/spring-feedback/src/generated/java/tum/devoops/feedbackservice/api/FeedbackApi.java b/services/spring-feedback/src/generated/java/tum/devoops/feedbackservice/api/FeedbackApi.java index 8b87aef..03ecfe3 100644 --- a/services/spring-feedback/src/generated/java/tum/devoops/feedbackservice/api/FeedbackApi.java +++ b/services/spring-feedback/src/generated/java/tum/devoops/feedbackservice/api/FeedbackApi.java @@ -51,6 +51,7 @@ default Optional getRequest() { /** * POST /feedback : Create feedback + * Creates a new feedback entry for a member. - Trainers: can create feedback for their trainees. - Admins: can create feedback for any member. * * @param feedbackCreate The request body for creating new feedback. (required) * @return The request was successful, and a new resource was created. (status code 201) @@ -63,6 +64,7 @@ default Optional getRequest() { @Operation( operationId = "createFeedback", summary = "Create feedback", + description = "Creates a new feedback entry for a member. - Trainers: can create feedback for their trainees. - Admins: can create feedback for any member. ", tags = { "feedback" }, responses = { @ApiResponse(responseCode = "201", description = "The request was successful, and a new resource was created.", content = { @@ -139,6 +141,7 @@ default ResponseEntity createFeedback( /** * DELETE /feedback/{feedback_id} : Delete feedback + * Deletes a specific feedback entry. - Creators: can delete feedback they submitted. - Admins: can delete any feedback. * * @param feedbackId (required) * @return The request was successful, but there is no content to return in the response. (status code 204) @@ -150,6 +153,7 @@ default ResponseEntity createFeedback( @Operation( operationId = "deleteFeedback", summary = "Delete feedback", + description = "Deletes a specific feedback entry. - Creators: can delete feedback they submitted. - Admins: can delete any feedback. ", tags = { "feedback" }, responses = { @ApiResponse(responseCode = "204", description = "The request was successful, but there is no content to return in the response."), @@ -210,6 +214,7 @@ default ResponseEntity deleteFeedback( /** * GET /feedback : Get all feedback + * Returns a list of all feedback entries. - Creators: can see feedback they submitted. - Members: can see feedback about themselves. - Admins: can see all feedback. * * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) * or Authentication is required to access the requested resource. The client must include the appropriate credentials. (status code 401) @@ -219,6 +224,7 @@ default ResponseEntity deleteFeedback( @Operation( operationId = "getAllFeedback", summary = "Get all feedback", + description = "Returns a list of all feedback entries. - Creators: can see feedback they submitted. - Members: can see feedback about themselves. - Admins: can see all feedback. ", tags = { "feedback" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -278,6 +284,7 @@ default ResponseEntity> getAllFeedback( /** * GET /feedback/{feedback_id} : Get feedback details + * Returns the details of a specific feedback entry. - Creators: can view feedback they submitted. - Members: can view feedback about themselves. - Admins: can view any feedback. * * @param feedbackId (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -289,6 +296,7 @@ default ResponseEntity> getAllFeedback( @Operation( operationId = "getFeedbackDetails", summary = "Get feedback details", + description = "Returns the details of a specific feedback entry. - Creators: can view feedback they submitted. - Members: can view feedback about themselves. - Admins: can view any feedback. ", tags = { "feedback" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -356,6 +364,7 @@ default ResponseEntity getFeedbackDetails( /** * PATCH /feedback/{feedback_id} : Update feedback details + * Partially updates a specific feedback entry. - Creators: can update feedback they submitted. - Admins: can update any feedback. * * @param feedbackId (required) * @param feedbackPartialUpdate The request body for partially updating a specific feedback. (required) @@ -369,6 +378,7 @@ default ResponseEntity getFeedbackDetails( @Operation( operationId = "updateFeedbackDetails", summary = "Update feedback details", + description = "Partially updates a specific feedback entry. - Creators: can update feedback they submitted. - Admins: can update any feedback. ", tags = { "feedback" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { diff --git a/services/spring-finance/src/generated/java/tum/devoops/financeservice/api/FinanceApi.java b/services/spring-finance/src/generated/java/tum/devoops/financeservice/api/FinanceApi.java index 14c0dda..26dd235 100644 --- a/services/spring-finance/src/generated/java/tum/devoops/financeservice/api/FinanceApi.java +++ b/services/spring-finance/src/generated/java/tum/devoops/financeservice/api/FinanceApi.java @@ -51,6 +51,7 @@ default Optional getRequest() { /** * POST /finance/transactions : Create transaction + * Creates a new financial transaction for a member. - Directors: can create transactions for members in their sport. - Admins: can create transactions for any member. * * @param transactionCreate The request body for creating a new transaction. (required) * @return The request was successful, and a new resource was created. (status code 201) @@ -63,6 +64,7 @@ default Optional getRequest() { @Operation( operationId = "createTransaction", summary = "Create transaction", + description = "Creates a new financial transaction for a member. - Directors: can create transactions for members in their sport. - Admins: can create transactions for any member. ", tags = { "finance" }, responses = { @ApiResponse(responseCode = "201", description = "The request was successful, and a new resource was created.", content = { @@ -139,6 +141,7 @@ default ResponseEntity createTransaction( /** * DELETE /finance/transactions/{transaction_id} : Delete transaction + * Deletes a specific transaction. - Creators: can delete transactions they created. - Admins: can delete any transaction. * * @param transactionId (required) * @return The request was successful, but there is no content to return in the response. (status code 204) @@ -150,6 +153,7 @@ default ResponseEntity createTransaction( @Operation( operationId = "deleteTransaction", summary = "Delete transaction", + description = "Deletes a specific transaction. - Creators: can delete transactions they created. - Admins: can delete any transaction. ", tags = { "finance" }, responses = { @ApiResponse(responseCode = "204", description = "The request was successful, but there is no content to return in the response."), @@ -210,6 +214,7 @@ default ResponseEntity deleteTransaction( /** * GET /finance/balances : Get all balances + * Returns a list of all member balances. - Directors: can view balances of members in their sport. - Admins: can view all balances. * * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) * or Authentication is required to access the requested resource. The client must include the appropriate credentials. (status code 401) @@ -219,6 +224,7 @@ default ResponseEntity deleteTransaction( @Operation( operationId = "getAllBalances", summary = "Get all balances", + description = "Returns a list of all member balances. - Directors: can view balances of members in their sport. - Admins: can view all balances. ", tags = { "finance" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -278,6 +284,7 @@ default ResponseEntity> getAllBalances( /** * GET /finance/transactions : Get all transactions + * Returns a list of all transactions. Users only see transactions where they are the member or the creator. - Members: can see transactions they are part of. - Creators: can see transactions they created. - Directors: can see transactions for members in their sport. - Admins: can see all transactions. * * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) * or Authentication is required to access the requested resource. The client must include the appropriate credentials. (status code 401) @@ -287,6 +294,7 @@ default ResponseEntity> getAllBalances( @Operation( operationId = "getAllTransactions", summary = "Get all transactions", + description = "Returns a list of all transactions. Users only see transactions where they are the member or the creator. - Members: can see transactions they are part of. - Creators: can see transactions they created. - Directors: can see transactions for members in their sport. - Admins: can see all transactions. ", tags = { "finance" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -346,6 +354,7 @@ default ResponseEntity> getAllTransactions( /** * GET /finance/balances/{member_id} : Get member balance + * Returns the balance of a specific member. - Members themselves: can view their own balance. - Directors: can view balances of members in their sport. - Admins: can view any member's balance. * * @param memberId (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -357,6 +366,7 @@ default ResponseEntity> getAllTransactions( @Operation( operationId = "getMemberBalance", summary = "Get member balance", + description = "Returns the balance of a specific member. - Members themselves: can view their own balance. - Directors: can view balances of members in their sport. - Admins: can view any member's balance. ", tags = { "finance" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -424,6 +434,7 @@ default ResponseEntity getMemberBalance( /** * GET /finance/transactions/{transaction_id} : Get transaction + * Returns the details of a specific transaction. - Members: can view transactions they are part of. - Creators: can view transactions they created. - Directors: can view transactions for members in their sport. - Admins: can view any transaction. * * @param transactionId (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -435,6 +446,7 @@ default ResponseEntity getMemberBalance( @Operation( operationId = "getTransaction", summary = "Get transaction", + description = "Returns the details of a specific transaction. - Members: can view transactions they are part of. - Creators: can view transactions they created. - Directors: can view transactions for members in their sport. - Admins: can view any transaction. ", tags = { "finance" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -502,6 +514,7 @@ default ResponseEntity getTransaction( /** * PATCH /finance/transactions/{transaction_id} : Update transaction + * Partially updates a specific transaction. The member field can only be changed by admins. - Creators: can update transactions they created (except the member field). - Admins: can update any transaction including the member field. * * @param transactionId (required) * @param transactionPartialUpdate The request body for partially updating a transaction. (required) @@ -515,6 +528,7 @@ default ResponseEntity getTransaction( @Operation( operationId = "updateTransaction", summary = "Update transaction", + description = "Partially updates a specific transaction. The member field can only be changed by admins. - Creators: can update transactions they created (except the member field). - Admins: can update any transaction including the member field. ", tags = { "finance" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { diff --git a/services/spring-letter/src/generated/java/tum/devoops/letterservice/api/LettersApi.java b/services/spring-letter/src/generated/java/tum/devoops/letterservice/api/LettersApi.java index 44886ba..57d5639 100644 --- a/services/spring-letter/src/generated/java/tum/devoops/letterservice/api/LettersApi.java +++ b/services/spring-letter/src/generated/java/tum/devoops/letterservice/api/LettersApi.java @@ -46,6 +46,7 @@ default Optional getRequest() { /** * POST /letters/pdf : Get pdf + * Generates and returns a PDF document from the provided HTML template. - Trainers: can generate PDFs related to their team. - Directors: can generate PDFs related to their sport. - Admins: can generate PDFs related to any member. * * @param body The request body for generating a pdf from a template. It must be a valid HTML string using the template format with placeholders for dynamic content. (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -57,6 +58,7 @@ default Optional getRequest() { @Operation( operationId = "getPdf", summary = "Get pdf", + description = "Generates and returns a PDF document from the provided HTML template. - Trainers: can generate PDFs related to their team. - Directors: can generate PDFs related to their sport. - Admins: can generate PDFs related to any member. ", tags = { "letters" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -125,6 +127,7 @@ default ResponseEntity getPdf( /** * POST /letters/mail : Send mail + * Sends an email based on the provided HTML template. - Trainers: can send mail to members of their team. - Directors: can send mail to members related to their sport. - Admins: can send mail to any member. * * @param body The request body for sending mail. It will be used in the email content. It must be a valid HTML string using the template format with placeholders for dynamic content. (required) * @return The request was successful, but there is no content to return in the response. (status code 204) @@ -136,6 +139,7 @@ default ResponseEntity getPdf( @Operation( operationId = "sendMail", summary = "Send mail", + description = "Sends an email based on the provided HTML template. - Trainers: can send mail to members of their team. - Directors: can send mail to members related to their sport. - Admins: can send mail to any member. ", tags = { "letters" }, responses = { @ApiResponse(responseCode = "204", description = "The request was successful, but there is no content to return in the response."), diff --git a/services/spring-member/src/generated/java/tum/devoops/memberservice/api/MembersApi.java b/services/spring-member/src/generated/java/tum/devoops/memberservice/api/MembersApi.java index cebbf57..9f7a8e6 100644 --- a/services/spring-member/src/generated/java/tum/devoops/memberservice/api/MembersApi.java +++ b/services/spring-member/src/generated/java/tum/devoops/memberservice/api/MembersApi.java @@ -51,6 +51,7 @@ default Optional getRequest() { /** * POST /members : Create member + * Creates a new member in the organization. Includes a password field for setting initial credentials. - Admins: can create members. * * @param memberCreate The request body for creating a new member. (required) * @return The request was successful, and a new resource was created. (status code 201) @@ -63,6 +64,7 @@ default Optional getRequest() { @Operation( operationId = "createMember", summary = "Create member", + description = "Creates a new member in the organization. Includes a password field for setting initial credentials. - Admins: can create members. ", tags = { "members" }, responses = { @ApiResponse(responseCode = "201", description = "The request was successful, and a new resource was created.", content = { @@ -139,6 +141,7 @@ default ResponseEntity createMember( /** * DELETE /members/{member_id} : Delete member + * Deletes a member from the organization. - Admins: can delete members. * * @param memberId (required) * @return The request was successful, but there is no content to return in the response. (status code 204) @@ -150,6 +153,7 @@ default ResponseEntity createMember( @Operation( operationId = "deleteMember", summary = "Delete member", + description = "Deletes a member from the organization. - Admins: can delete members. ", tags = { "members" }, responses = { @ApiResponse(responseCode = "204", description = "The request was successful, but there is no content to return in the response."), @@ -210,6 +214,7 @@ default ResponseEntity deleteMember( /** * GET /members : Get all members + * Returns a list of all members in the organization. - All authenticated users: can access this endpoint. * * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) * or Authentication is required to access the requested resource. The client must include the appropriate credentials. (status code 401) @@ -219,6 +224,7 @@ default ResponseEntity deleteMember( @Operation( operationId = "getAllMembers", summary = "Get all members", + description = "Returns a list of all members in the organization. - All authenticated users: can access this endpoint. ", tags = { "members" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -278,6 +284,7 @@ default ResponseEntity> getAllMembers( /** * GET /members/{member_id} : Get member details + * Returns the full details of a specific member. - Members themselves: can view their own details. - Team members: can view details of others in the same team. - Trainers: can view details of members in their team. - Directors: can view details of members in their sport. - Admins: can view any member's details. * * @param memberId (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -289,6 +296,7 @@ default ResponseEntity> getAllMembers( @Operation( operationId = "getMemberDetails", summary = "Get member details", + description = "Returns the full details of a specific member. - Members themselves: can view their own details. - Team members: can view details of others in the same team. - Trainers: can view details of members in their team. - Directors: can view details of members in their sport. - Admins: can view any member's details. ", tags = { "members" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -356,6 +364,7 @@ default ResponseEntity getMemberDetails( /** * PATCH /members/{member_id} : Update member details + * Partially updates the details of a specific member. - Members themselves: can update their own details. - Admins: can update any member's details. * * @param memberId (required) * @param memberPartialUpdate The request body for partially updating a member. (required) @@ -369,6 +378,7 @@ default ResponseEntity getMemberDetails( @Operation( operationId = "updateMemberDetails", summary = "Update member details", + description = "Partially updates the details of a specific member. - Members themselves: can update their own details. - Admins: can update any member's details. ", tags = { "members" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { diff --git a/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java b/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java index c4c2cac..561edbe 100644 --- a/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java +++ b/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java @@ -30,6 +30,8 @@ public class MemberCreate { private String email; + private String password; + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private @Nullable LocalDate birthday; @@ -46,10 +48,11 @@ public MemberCreate() { /** * Constructor with only required parameters */ - public MemberCreate(String firstName, String lastName, String email) { + public MemberCreate(String firstName, String lastName, String email, String password) { this.firstName = firstName; this.lastName = lastName; this.email = email; + this.password = password; } public MemberCreate firstName(String firstName) { @@ -112,6 +115,26 @@ public void setEmail(String email) { this.email = email; } + public MemberCreate password(String password) { + this.password = password; + return this; + } + + /** + * Get password + * @return password + */ + @NotNull + @Schema(name = "password", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty("password") + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + public MemberCreate birthday(@Nullable LocalDate birthday) { this.birthday = birthday; return this; @@ -204,6 +227,7 @@ public boolean equals(Object o) { return Objects.equals(this.firstName, memberCreate.firstName) && Objects.equals(this.lastName, memberCreate.lastName) && Objects.equals(this.email, memberCreate.email) && + Objects.equals(this.password, memberCreate.password) && Objects.equals(this.birthday, memberCreate.birthday) && Objects.equals(this.phoneNumber, memberCreate.phoneNumber) && Objects.equals(this.address, memberCreate.address) && @@ -212,7 +236,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(firstName, lastName, email, birthday, phoneNumber, address, information); + return Objects.hash(firstName, lastName, email, password, birthday, phoneNumber, address, information); } @Override @@ -222,6 +246,7 @@ public String toString() { sb.append(" firstName: ").append(toIndentedString(firstName)).append("\n"); sb.append(" lastName: ").append(toIndentedString(lastName)).append("\n"); sb.append(" email: ").append(toIndentedString(email)).append("\n"); + sb.append(" password: ").append("*").append("\n"); sb.append(" birthday: ").append(toIndentedString(birthday)).append("\n"); sb.append(" phoneNumber: ").append(toIndentedString(phoneNumber)).append("\n"); sb.append(" address: ").append(toIndentedString(address)).append("\n"); diff --git a/services/spring-organization/src/generated/java/tum/devoops/organizationservice/api/OrganizationApi.java b/services/spring-organization/src/generated/java/tum/devoops/organizationservice/api/OrganizationApi.java index f1e864b..bc0c48a 100644 --- a/services/spring-organization/src/generated/java/tum/devoops/organizationservice/api/OrganizationApi.java +++ b/services/spring-organization/src/generated/java/tum/devoops/organizationservice/api/OrganizationApi.java @@ -53,7 +53,7 @@ default Optional getRequest() { /** * POST /organization/sports : Create sport - * Creates a new sport in the organization. Only admins are allowed to create new sports. + * Creates a new sport in the organization. - Admins: can create sports. * * @param sportCreate The request body for creating a new sport. (required) * @return The request was successful, and a new resource was created. (status code 201) @@ -66,7 +66,7 @@ default Optional getRequest() { @Operation( operationId = "createSport", summary = "Create sport", - description = "Creates a new sport in the organization. Only admins are allowed to create new sports.", + description = "Creates a new sport in the organization. - Admins: can create sports. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "201", description = "The request was successful, and a new resource was created.", content = { @@ -143,6 +143,7 @@ default ResponseEntity createSport( /** * POST /organization/teams : Create team + * Creates a new team in the organization. - Directors: can create teams for their own sport. - Admins: can create teams for any sport. * * @param teamCreate The request body for creating a new team. (required) * @return The request was successful, and a new resource was created. (status code 201) @@ -155,6 +156,7 @@ default ResponseEntity createSport( @Operation( operationId = "createTeam", summary = "Create team", + description = "Creates a new team in the organization. - Directors: can create teams for their own sport. - Admins: can create teams for any sport. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "201", description = "The request was successful, and a new resource was created.", content = { @@ -231,6 +233,7 @@ default ResponseEntity createTeam( /** * DELETE /organization/sports/{sport_name} : Delete sport + * Deletes a sport from the organization. - Admins: can delete sports. * * @param sportName (required) * @return The request was successful, but there is no content to return in the response. (status code 204) @@ -242,6 +245,7 @@ default ResponseEntity createTeam( @Operation( operationId = "deleteSport", summary = "Delete sport", + description = "Deletes a sport from the organization. - Admins: can delete sports. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "204", description = "The request was successful, but there is no content to return in the response."), @@ -302,6 +306,7 @@ default ResponseEntity deleteSport( /** * DELETE /organization/teams/{team_id} : Delete team + * Deletes a team from the organization. - Directors: can delete teams in their sport. - Admins: can delete any team. * * @param teamId (required) * @return The request was successful, but there is no content to return in the response. (status code 204) @@ -313,6 +318,7 @@ default ResponseEntity deleteSport( @Operation( operationId = "deleteTeam", summary = "Delete team", + description = "Deletes a team from the organization. - Directors: can delete teams in their sport. - Admins: can delete any team. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "204", description = "The request was successful, but there is no content to return in the response."), @@ -373,7 +379,7 @@ default ResponseEntity deleteTeam( /** * GET /organization/sports : Get all sports - * Returns a list of all sports registered in the organization. If the caller is not an admin, only sports that the caller is a member of will be returned. + * Returns a list of all sports registered in the organization. - All authenticated users: can access this endpoint. * * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) * or Authentication is required to access the requested resource. The client must include the appropriate credentials. (status code 401) @@ -383,7 +389,7 @@ default ResponseEntity deleteTeam( @Operation( operationId = "getAllSports", summary = "Get all sports", - description = "Returns a list of all sports registered in the organization. If the caller is not an admin, only sports that the caller is a member of will be returned.", + description = "Returns a list of all sports registered in the organization. - All authenticated users: can access this endpoint. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -443,6 +449,7 @@ default ResponseEntity> getAllSports( /** * GET /organization/teams : Get all teams + * Returns a list of all teams in the organization. - All authenticated users: can access this endpoint. * * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) * or Authentication is required to access the requested resource. The client must include the appropriate credentials. (status code 401) @@ -452,6 +459,7 @@ default ResponseEntity> getAllSports( @Operation( operationId = "getAllTeams", summary = "Get all teams", + description = "Returns a list of all teams in the organization. - All authenticated users: can access this endpoint. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -511,6 +519,7 @@ default ResponseEntity> getAllTeams( /** * GET /organization/sports/{sport_name} : Get sport + * Returns the details of a specific sport. - All authenticated users: can access this endpoint. * * @param sportName (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -522,6 +531,7 @@ default ResponseEntity> getAllTeams( @Operation( operationId = "getSport", summary = "Get sport", + description = "Returns the details of a specific sport. - All authenticated users: can access this endpoint. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -589,6 +599,7 @@ default ResponseEntity getSport( /** * GET /organization/teams/{team_id} : Get team + * Returns the details of a specific team. - All authenticated users: can access this endpoint. * * @param teamId (required) * @return The request was successful, and the server has returned the requested resource in the response body. (status code 200) @@ -600,6 +611,7 @@ default ResponseEntity getSport( @Operation( operationId = "getTeam", summary = "Get team", + description = "Returns the details of a specific team. - All authenticated users: can access this endpoint. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -667,6 +679,7 @@ default ResponseEntity getTeam( /** * PATCH /organization/sports/{sport_name} : Update sport + * Partially updates an existing sport's details. - Directors: can update all fields except directors. - Admins: can update all fields. * * @param sportName (required) * @param sportPartialUpdate The request body for partially updating a sport. (required) @@ -680,6 +693,7 @@ default ResponseEntity getTeam( @Operation( operationId = "updateSport", summary = "Update sport", + description = "Partially updates an existing sport's details. - Directors: can update all fields except directors. - Admins: can update all fields. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { @@ -757,6 +771,7 @@ default ResponseEntity updateSport( /** * PATCH /organization/teams/{team_id} : Update team + * Partially updates a team's details. - Trainers: can update all fields except sport and trainers. - Directors: can update all fields except sport. - Admins: can update all fields. * * @param teamId (required) * @param teamPartialUpdate The request body for partially updating a team. (required) @@ -770,6 +785,7 @@ default ResponseEntity updateSport( @Operation( operationId = "updateTeam", summary = "Update team", + description = "Partially updates a team's details. - Trainers: can update all fields except sport and trainers. - Directors: can update all fields except sport. - Admins: can update all fields. ", tags = { "organization" }, responses = { @ApiResponse(responseCode = "200", description = "The request was successful, and the server has returned the requested resource in the response body.", content = { diff --git a/web-client/src/api.ts b/web-client/src/api.ts index b4a6582..1e23e85 100644 --- a/web-client/src/api.ts +++ b/web-client/src/api.ts @@ -13,13 +13,15 @@ export interface paths { }; /** * Get all sports - * @description Returns a list of all sports registered in the organization. If the caller is not an admin, only sports that the caller is a member of will be returned. + * @description Returns a list of all sports registered in the organization. + * - All authenticated users: can access this endpoint. */ get: operations["getAllSports"]; put?: never; /** * Create sport - * @description Creates a new sport in the organization. Only admins are allowed to create new sports. + * @description Creates a new sport in the organization. + * - Admins: can create sports. */ post: operations["createSport"]; delete?: never; @@ -35,15 +37,28 @@ export interface paths { path?: never; cookie?: never; }; - /** Get sport */ + /** + * Get sport + * @description Returns the details of a specific sport. + * - All authenticated users: can access this endpoint. + */ get: operations["getSport"]; put?: never; post?: never; - /** Delete sport */ + /** + * Delete sport + * @description Deletes a sport from the organization. + * - Admins: can delete sports. + */ delete: operations["deleteSport"]; options?: never; head?: never; - /** Update sport */ + /** + * Update sport + * @description Partially updates an existing sport's details. + * - Directors: can update all fields except directors. + * - Admins: can update all fields. + */ patch: operations["updateSport"]; trace?: never; }; @@ -54,10 +69,19 @@ export interface paths { path?: never; cookie?: never; }; - /** Get all teams */ + /** + * Get all teams + * @description Returns a list of all teams in the organization. + * - All authenticated users: can access this endpoint. + */ get: operations["getAllTeams"]; put?: never; - /** Create team */ + /** + * Create team + * @description Creates a new team in the organization. + * - Directors: can create teams for their own sport. + * - Admins: can create teams for any sport. + */ post: operations["createTeam"]; delete?: never; options?: never; @@ -72,15 +96,30 @@ export interface paths { path?: never; cookie?: never; }; - /** Get team */ + /** + * Get team + * @description Returns the details of a specific team. + * - All authenticated users: can access this endpoint. + */ get: operations["getTeam"]; put?: never; post?: never; - /** Delete team */ + /** + * Delete team + * @description Deletes a team from the organization. + * - Directors: can delete teams in their sport. + * - Admins: can delete any team. + */ delete: operations["deleteTeam"]; options?: never; head?: never; - /** Update team */ + /** + * Update team + * @description Partially updates a team's details. + * - Trainers: can update all fields except sport and trainers. + * - Directors: can update all fields except sport. + * - Admins: can update all fields. + */ patch: operations["updateTeam"]; trace?: never; }; @@ -91,10 +130,18 @@ export interface paths { path?: never; cookie?: never; }; - /** Get all members */ + /** + * Get all members + * @description Returns a list of all members in the organization. + * - All authenticated users: can access this endpoint. + */ get: operations["getAllMembers"]; put?: never; - /** Create member */ + /** + * Create member + * @description Creates a new member in the organization. Includes a password field for setting initial credentials. + * - Admins: can create members. + */ post: operations["createMember"]; delete?: never; options?: never; @@ -109,15 +156,32 @@ export interface paths { path?: never; cookie?: never; }; - /** Get member details */ + /** + * Get member details + * @description Returns the full details of a specific member. + * - Members themselves: can view their own details. + * - Team members: can view details of others in the same team. + * - Trainers: can view details of members in their team. + * - Directors: can view details of members in their sport. + * - Admins: can view any member's details. + */ get: operations["getMemberDetails"]; put?: never; post?: never; - /** Delete member */ + /** + * Delete member + * @description Deletes a member from the organization. + * - Admins: can delete members. + */ delete: operations["deleteMember"]; options?: never; head?: never; - /** Update member details */ + /** + * Update member details + * @description Partially updates the details of a specific member. + * - Members themselves: can update their own details. + * - Admins: can update any member's details. + */ patch: operations["updateMemberDetails"]; trace?: never; }; @@ -128,10 +192,22 @@ export interface paths { path?: never; cookie?: never; }; - /** Get all events */ + /** + * Get all events + * @description Returns a list of all events. + * - All authenticated users: can see events linked to their team or sport, or events where they are an attendee. + * - Creators: can see all events they created. + * - Admins: can see all events. + */ get: operations["getAllEvents"]; put?: never; - /** Create event */ + /** + * Create event + * @description Creates a new event. + * - Directors: can create events for their sport. + * - Trainers: can create events for their team. + * - Admins: can create any event. + */ post: operations["createEvent"]; delete?: never; options?: never; @@ -146,15 +222,33 @@ export interface paths { path?: never; cookie?: never; }; - /** Get event details */ + /** + * Get event details + * @description Returns the details of a specific event. + * - All authenticated users: can access events linked to their team or sport, or events where they are an attendee. + * - Creators: can view events they created. + * - Admins: can view any event. + */ get: operations["getEventDetails"]; put?: never; post?: never; - /** Delete event */ + /** + * Delete event + * @description Deletes a specific event. + * - Creators: can delete events they created. + * - Directors: can delete events linked to their sport. + * - Admins: can delete any event. + */ delete: operations["deleteEvent"]; options?: never; head?: never; - /** Update event details */ + /** + * Update event details + * @description Partially updates the details of a specific event. + * - Creators: can update events they created. + * - Directors: can update events linked to their sport. + * - Admins: can update any event. + */ patch: operations["updateEventDetails"]; trace?: never; }; @@ -165,10 +259,21 @@ export interface paths { path?: never; cookie?: never; }; - /** Get all feedback */ + /** + * Get all feedback + * @description Returns a list of all feedback entries. + * - Creators: can see feedback they submitted. + * - Members: can see feedback about themselves. + * - Admins: can see all feedback. + */ get: operations["getAllFeedback"]; put?: never; - /** Create feedback */ + /** + * Create feedback + * @description Creates a new feedback entry for a member. + * - Trainers: can create feedback for their trainees. + * - Admins: can create feedback for any member. + */ post: operations["createFeedback"]; delete?: never; options?: never; @@ -183,15 +288,31 @@ export interface paths { path?: never; cookie?: never; }; - /** Get feedback details */ + /** + * Get feedback details + * @description Returns the details of a specific feedback entry. + * - Creators: can view feedback they submitted. + * - Members: can view feedback about themselves. + * - Admins: can view any feedback. + */ get: operations["getFeedbackDetails"]; put?: never; post?: never; - /** Delete feedback */ + /** + * Delete feedback + * @description Deletes a specific feedback entry. + * - Creators: can delete feedback they submitted. + * - Admins: can delete any feedback. + */ delete: operations["deleteFeedback"]; options?: never; head?: never; - /** Update feedback details */ + /** + * Update feedback details + * @description Partially updates a specific feedback entry. + * - Creators: can update feedback they submitted. + * - Admins: can update any feedback. + */ patch: operations["updateFeedbackDetails"]; trace?: never; }; @@ -202,7 +323,12 @@ export interface paths { path?: never; cookie?: never; }; - /** Get all balances */ + /** + * Get all balances + * @description Returns a list of all member balances. + * - Directors: can view balances of members in their sport. + * - Admins: can view all balances. + */ get: operations["getAllBalances"]; put?: never; post?: never; @@ -219,7 +345,13 @@ export interface paths { path?: never; cookie?: never; }; - /** Get member balance */ + /** + * Get member balance + * @description Returns the balance of a specific member. + * - Members themselves: can view their own balance. + * - Directors: can view balances of members in their sport. + * - Admins: can view any member's balance. + */ get: operations["getMemberBalance"]; put?: never; post?: never; @@ -236,10 +368,22 @@ export interface paths { path?: never; cookie?: never; }; - /** Get all transactions */ + /** + * Get all transactions + * @description Returns a list of all transactions. Users only see transactions where they are the member or the creator. + * - Members: can see transactions they are part of. + * - Creators: can see transactions they created. + * - Directors: can see transactions for members in their sport. + * - Admins: can see all transactions. + */ get: operations["getAllTransactions"]; put?: never; - /** Create transaction */ + /** + * Create transaction + * @description Creates a new financial transaction for a member. + * - Directors: can create transactions for members in their sport. + * - Admins: can create transactions for any member. + */ post: operations["createTransaction"]; delete?: never; options?: never; @@ -254,15 +398,32 @@ export interface paths { path?: never; cookie?: never; }; - /** Get transaction */ + /** + * Get transaction + * @description Returns the details of a specific transaction. + * - Members: can view transactions they are part of. + * - Creators: can view transactions they created. + * - Directors: can view transactions for members in their sport. + * - Admins: can view any transaction. + */ get: operations["getTransaction"]; put?: never; post?: never; - /** Delete transaction */ + /** + * Delete transaction + * @description Deletes a specific transaction. + * - Creators: can delete transactions they created. + * - Admins: can delete any transaction. + */ delete: operations["deleteTransaction"]; options?: never; head?: never; - /** Update transaction */ + /** + * Update transaction + * @description Partially updates a specific transaction. The member field can only be changed by admins. + * - Creators: can update transactions they created (except the member field). + * - Admins: can update any transaction including the member field. + */ patch: operations["updateTransaction"]; trace?: never; }; @@ -275,7 +436,13 @@ export interface paths { }; get?: never; put?: never; - /** Send mail */ + /** + * Send mail + * @description Sends an email based on the provided HTML template. + * - Trainers: can send mail to members of their team. + * - Directors: can send mail to members related to their sport. + * - Admins: can send mail to any member. + */ post: operations["sendMail"]; delete?: never; options?: never; @@ -292,7 +459,13 @@ export interface paths { }; get?: never; put?: never; - /** Get pdf */ + /** + * Get pdf + * @description Generates and returns a PDF document from the provided HTML template. + * - Trainers: can generate PDFs related to their team. + * - Directors: can generate PDFs related to their sport. + * - Admins: can generate PDFs related to any member. + */ post: operations["getPdf"]; delete?: never; options?: never; @@ -307,7 +480,13 @@ export interface paths { path?: never; cookie?: never; }; - /** Generate report */ + /** + * Generate report + * @description Generates an AI-based report for a member. Members can only generate reports for themselves. + * - All authenticated users: can generate a report for themselves. + * - Trainers: can generate reports for members of their team. + * - Admin: can generate a report for any member. + */ get: operations["generateReport"]; put?: never; post?: never; @@ -418,6 +597,8 @@ export interface components { first_name: string; last_name: string; email: string; + /** Format: password */ + password: string; /** Format: date */ birthday?: string; phone_number?: string; From 369ed266741381ce9f672807686f4c83ad30db73 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Fri, 19 Jun 2026 10:49:11 +0200 Subject: [PATCH 2/7] feat: add cross-schema read entities, repositories, and exception handling --- .../config/checkstyle/checkstyle.xml | 7 ++++ .../feedbackservice/entity/EventEntity.java | 21 ++++++++++ .../feedbackservice/entity/MemberEntity.java | 21 ++++++++++ .../feedbackservice/entity/TraineeEntity.java | 40 +++++++++++++++++++ .../feedbackservice/entity/TrainerEntity.java | 40 +++++++++++++++++++ .../exception/BadRequestException.java | 8 ++++ .../exception/ForbiddenException.java | 8 ++++ .../exception/GlobalExceptionHandler.java | 31 ++++++++++++++ .../exception/NotFoundException.java | 8 ++++ .../repository/EventRepository.java | 10 +++++ .../repository/MemberRepository.java | 10 +++++ .../repository/TraineeRepository.java | 14 +++++++ .../repository/TrainerRepository.java | 14 +++++++ 13 files changed, 232 insertions(+) create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/EventEntity.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/MemberEntity.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TraineeEntity.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TrainerEntity.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/BadRequestException.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/ForbiddenException.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/GlobalExceptionHandler.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/NotFoundException.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/EventRepository.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/MemberRepository.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TraineeRepository.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TrainerRepository.java diff --git a/services/spring-feedback/config/checkstyle/checkstyle.xml b/services/spring-feedback/config/checkstyle/checkstyle.xml index 580dda9..77ea37c 100644 --- a/services/spring-feedback/config/checkstyle/checkstyle.xml +++ b/services/spring-feedback/config/checkstyle/checkstyle.xml @@ -15,6 +15,13 @@ + + + + + + diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/EventEntity.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/EventEntity.java new file mode 100644 index 0000000..af846e0 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/EventEntity.java @@ -0,0 +1,21 @@ +package tum.devoops.feedbackservice.entity; + +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(schema = "event", name = "events") +@Getter +@NoArgsConstructor +public class EventEntity { + + @Id + @Column(name = "id", nullable = false, updatable = false) + private UUID id; +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/MemberEntity.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/MemberEntity.java new file mode 100644 index 0000000..8665e66 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/MemberEntity.java @@ -0,0 +1,21 @@ +package tum.devoops.feedbackservice.entity; + +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(schema = "member", name = "members") +@Getter +@NoArgsConstructor +public class MemberEntity { + + @Id + @Column(name = "id", nullable = false, updatable = false) + private UUID id; +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TraineeEntity.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TraineeEntity.java new file mode 100644 index 0000000..4485bd4 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TraineeEntity.java @@ -0,0 +1,40 @@ +package tum.devoops.feedbackservice.entity; + +import java.io.Serializable; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(schema = "organization", name = "trainees") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class TraineeEntity { + + @EmbeddedId + private Id id; + + @Embeddable + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Id implements Serializable { + + @Column(name = "team_id", nullable = false) + private UUID teamId; + + @Column(name = "member_id", nullable = false) + private UUID memberId; + } +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TrainerEntity.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TrainerEntity.java new file mode 100644 index 0000000..429b693 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/TrainerEntity.java @@ -0,0 +1,40 @@ +package tum.devoops.feedbackservice.entity; + +import java.io.Serializable; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(schema = "organization", name = "trainers") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class TrainerEntity { + + @EmbeddedId + private Id id; + + @Embeddable + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Id implements Serializable { + + @Column(name = "team_id", nullable = false) + private UUID teamId; + + @Column(name = "member_id", nullable = false) + private UUID memberId; + } +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/BadRequestException.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/BadRequestException.java new file mode 100644 index 0000000..6de47f6 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package tum.devoops.feedbackservice.exception; + +public class BadRequestException extends RuntimeException { + + public BadRequestException(String message) { + super(message); + } +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/ForbiddenException.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/ForbiddenException.java new file mode 100644 index 0000000..43dfeb5 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/ForbiddenException.java @@ -0,0 +1,8 @@ +package tum.devoops.feedbackservice.exception; + +public class ForbiddenException extends RuntimeException { + + public ForbiddenException(String message) { + super(message); + } +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/GlobalExceptionHandler.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..95ae643 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/GlobalExceptionHandler.java @@ -0,0 +1,31 @@ +package tum.devoops.feedbackservice.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import tum.devoops.feedbackservice.model.BadRequestResponse; +import tum.devoops.feedbackservice.model.ErrorResponse; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFound(NotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse().message(ex.getMessage())); + } + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbidden(ForbiddenException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse().message(ex.getMessage())); + } + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequest(BadRequestException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new BadRequestResponse().message(ex.getMessage())); + } +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/NotFoundException.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/NotFoundException.java new file mode 100644 index 0000000..a8710eb --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package tum.devoops.feedbackservice.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/EventRepository.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/EventRepository.java new file mode 100644 index 0000000..37de08f --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/EventRepository.java @@ -0,0 +1,10 @@ +package tum.devoops.feedbackservice.repository; + +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +import tum.devoops.feedbackservice.entity.EventEntity; + +public interface EventRepository extends JpaRepository { +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/MemberRepository.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/MemberRepository.java new file mode 100644 index 0000000..d8924ce --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/MemberRepository.java @@ -0,0 +1,10 @@ +package tum.devoops.feedbackservice.repository; + +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +import tum.devoops.feedbackservice.entity.MemberEntity; + +public interface MemberRepository extends JpaRepository { +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TraineeRepository.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TraineeRepository.java new file mode 100644 index 0000000..3813972 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TraineeRepository.java @@ -0,0 +1,14 @@ +package tum.devoops.feedbackservice.repository; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +import tum.devoops.feedbackservice.entity.TraineeEntity; + +public interface TraineeRepository extends JpaRepository { + + // SELECT * FROM organization.trainees WHERE member_id = ? + List findAllById_MemberId(UUID memberId); +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TrainerRepository.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TrainerRepository.java new file mode 100644 index 0000000..dae9ea2 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/TrainerRepository.java @@ -0,0 +1,14 @@ +package tum.devoops.feedbackservice.repository; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +import tum.devoops.feedbackservice.entity.TrainerEntity; + +public interface TrainerRepository extends JpaRepository { + + // SELECT * FROM organization.trainers WHERE member_id = ? + List findAllById_MemberId(UUID memberId); +} From 9169e5d7658cdb080582e20feb7abf6a7ef9c7d4 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Fri, 19 Jun 2026 11:00:59 +0200 Subject: [PATCH 3/7] feat: implement GET /feedback and GET /feedback/{feedback_id} --- .../controller/FeedbackController.java | 51 +++++++++ .../service/FeedbackService.java | 105 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java create mode 100644 services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java new file mode 100644 index 0000000..6521a61 --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java @@ -0,0 +1,51 @@ +package tum.devoops.feedbackservice.controller; + +import java.util.List; +import java.util.UUID; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.RestController; + +import tum.devoops.feedbackservice.api.FeedbackApi; +import tum.devoops.feedbackservice.model.Feedback; +import tum.devoops.feedbackservice.model.FeedbackSummary; +import tum.devoops.feedbackservice.service.FeedbackService; + +@RestController +public class FeedbackController implements FeedbackApi { + + private final FeedbackService feedbackService; + + public FeedbackController(FeedbackService feedbackService) { + this.feedbackService = feedbackService; + } + + @Override + public ResponseEntity> getAllFeedback() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + UUID requesterId = extractRequesterId(auth); + boolean isAdmin = extractIsAdmin(auth); + return ResponseEntity.ok(feedbackService.getAllFeedback(requesterId, isAdmin)); + } + + @Override + public ResponseEntity getFeedbackDetails(UUID feedbackId) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + UUID requesterId = extractRequesterId(auth); + boolean isAdmin = extractIsAdmin(auth); + return ResponseEntity.ok(feedbackService.getFeedbackDetails(feedbackId, requesterId, isAdmin)); + } + + private UUID extractRequesterId(Authentication auth) { + Jwt jwt = (Jwt) auth.getPrincipal(); + return UUID.fromString(jwt.getSubject()); + } + + private boolean extractIsAdmin(Authentication auth) { + return auth.getAuthorities().stream() + .anyMatch(a -> "ROLE_admin".equals(a.getAuthority())); + } +} diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java new file mode 100644 index 0000000..70de65b --- /dev/null +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java @@ -0,0 +1,105 @@ +package tum.devoops.feedbackservice.service; + +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import tum.devoops.feedbackservice.entity.FeedbackEntity; +import tum.devoops.feedbackservice.exception.ForbiddenException; +import tum.devoops.feedbackservice.exception.NotFoundException; +import tum.devoops.feedbackservice.model.Feedback; +import tum.devoops.feedbackservice.model.FeedbackSummary; +import tum.devoops.feedbackservice.repository.EventRepository; +import tum.devoops.feedbackservice.repository.FeedbackRepository; +import tum.devoops.feedbackservice.repository.MemberRepository; +import tum.devoops.feedbackservice.repository.TraineeRepository; +import tum.devoops.feedbackservice.repository.TrainerRepository; + +@Service +public class FeedbackService { + + private final FeedbackRepository feedbackRepository; + private final EventRepository eventRepository; + private final MemberRepository memberRepository; + private final TrainerRepository trainerRepository; + private final TraineeRepository traineeRepository; + + public FeedbackService( + FeedbackRepository feedbackRepository, + EventRepository eventRepository, + MemberRepository memberRepository, + TrainerRepository trainerRepository, + TraineeRepository traineeRepository) { + this.feedbackRepository = feedbackRepository; + this.eventRepository = eventRepository; + this.memberRepository = memberRepository; + this.trainerRepository = trainerRepository; + this.traineeRepository = traineeRepository; + } + + @Transactional(readOnly = true) + public List getAllFeedback(UUID requesterId, boolean isAdmin) { + List entities; + if (isAdmin) { + entities = feedbackRepository.findAll(); + } else { + Set seen = new HashSet<>(); + entities = new ArrayList<>(); + for (FeedbackEntity entity : feedbackRepository.findAllByCreatorId(requesterId)) { + if (seen.add(entity.getId())) { + entities.add(entity); + } + } + for (FeedbackEntity entity : feedbackRepository.findAllByMemberId(requesterId)) { + if (seen.add(entity.getId())) { + entities.add(entity); + } + } + } + return entities.stream().map(this::toFeedbackSummary).collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public Feedback getFeedbackDetails(UUID feedbackId, UUID requesterId, boolean isAdmin) { + FeedbackEntity entity = findFeedbackOrThrow(feedbackId); + boolean isCreator = requesterId.equals(entity.getCreatorId()); + boolean isMember = requesterId.equals(entity.getMemberId()); + if (!isAdmin && !isCreator && !isMember) { + throw new ForbiddenException("Access denied"); + } + return toFeedback(entity); + } + + private FeedbackEntity findFeedbackOrThrow(UUID feedbackId) { + return feedbackRepository.findById(feedbackId) + .orElseThrow(() -> new NotFoundException("Feedback not found: " + feedbackId)); + } + + private Feedback toFeedback(FeedbackEntity entity) { + return new Feedback( + entity.getId(), + entity.getEventId().toString(), + entity.getMemberId().toString(), + entity.getCreatorId().toString(), + entity.getCreatedAt().atOffset(ZoneOffset.UTC), + entity.getFeedback() + ); + } + + private FeedbackSummary toFeedbackSummary(FeedbackEntity entity) { + return new FeedbackSummary( + entity.getId(), + entity.getEventId().toString(), + entity.getMemberId().toString(), + entity.getCreatorId().toString(), + entity.getCreatedAt().atOffset(ZoneOffset.UTC) + ); + } +} From 03a07c691b5c8e1efc5a893ac04eab81496f8988 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Fri, 19 Jun 2026 11:02:57 +0200 Subject: [PATCH 4/7] feat: implement POST, PATCH, and DELETE /feedback endpoints --- .../controller/FeedbackController.java | 29 ++++++ .../service/FeedbackService.java | 91 +++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java index 6521a61..701714e 100644 --- a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.UUID; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -11,6 +12,8 @@ import tum.devoops.feedbackservice.api.FeedbackApi; import tum.devoops.feedbackservice.model.Feedback; +import tum.devoops.feedbackservice.model.FeedbackCreate; +import tum.devoops.feedbackservice.model.FeedbackPartialUpdate; import tum.devoops.feedbackservice.model.FeedbackSummary; import tum.devoops.feedbackservice.service.FeedbackService; @@ -31,6 +34,15 @@ public ResponseEntity> getAllFeedback() { return ResponseEntity.ok(feedbackService.getAllFeedback(requesterId, isAdmin)); } + @Override + public ResponseEntity createFeedback(FeedbackCreate feedbackCreate) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + UUID requesterId = extractRequesterId(auth); + boolean isAdmin = extractIsAdmin(auth); + Feedback created = feedbackService.createFeedback(feedbackCreate, requesterId, isAdmin); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + @Override public ResponseEntity getFeedbackDetails(UUID feedbackId) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); @@ -39,6 +51,23 @@ public ResponseEntity getFeedbackDetails(UUID feedbackId) { return ResponseEntity.ok(feedbackService.getFeedbackDetails(feedbackId, requesterId, isAdmin)); } + @Override + public ResponseEntity updateFeedbackDetails(UUID feedbackId, FeedbackPartialUpdate feedbackPartialUpdate) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + UUID requesterId = extractRequesterId(auth); + boolean isAdmin = extractIsAdmin(auth); + return ResponseEntity.ok(feedbackService.updateFeedbackDetails(feedbackId, feedbackPartialUpdate, requesterId, isAdmin)); + } + + @Override + public ResponseEntity deleteFeedback(UUID feedbackId) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + UUID requesterId = extractRequesterId(auth); + boolean isAdmin = extractIsAdmin(auth); + feedbackService.deleteFeedback(feedbackId, requesterId, isAdmin); + return ResponseEntity.noContent().build(); + } + private UUID extractRequesterId(Authentication auth) { Jwt jwt = (Jwt) auth.getPrincipal(); return UUID.fromString(jwt.getSubject()); diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java index 70de65b..d5b3f8e 100644 --- a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java @@ -1,5 +1,6 @@ package tum.devoops.feedbackservice.service; +import java.time.Instant; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.HashSet; @@ -12,9 +13,12 @@ import org.springframework.transaction.annotation.Transactional; import tum.devoops.feedbackservice.entity.FeedbackEntity; +import tum.devoops.feedbackservice.exception.BadRequestException; import tum.devoops.feedbackservice.exception.ForbiddenException; import tum.devoops.feedbackservice.exception.NotFoundException; import tum.devoops.feedbackservice.model.Feedback; +import tum.devoops.feedbackservice.model.FeedbackCreate; +import tum.devoops.feedbackservice.model.FeedbackPartialUpdate; import tum.devoops.feedbackservice.model.FeedbackSummary; import tum.devoops.feedbackservice.repository.EventRepository; import tum.devoops.feedbackservice.repository.FeedbackRepository; @@ -66,6 +70,32 @@ public List getAllFeedback(UUID requesterId, boolean isAdmin) { return entities.stream().map(this::toFeedbackSummary).collect(Collectors.toList()); } + @Transactional + public Feedback createFeedback(FeedbackCreate body, UUID requesterId, boolean isAdmin) { + UUID eventId = parseUuid(body.getEvent(), "event"); + UUID memberId = parseUuid(body.getMember(), "member"); + + if (!eventRepository.existsById(eventId)) { + throw new BadRequestException("Event not found: " + eventId); + } + if (!memberRepository.existsById(memberId)) { + throw new BadRequestException("Member not found: " + memberId); + } + + if (!isAdmin) { + assertTrainerOfMember(requesterId, memberId); + } + + FeedbackEntity entity = new FeedbackEntity(); + entity.setEventId(eventId); + entity.setMemberId(memberId); + entity.setCreatorId(requesterId); + entity.setCreatedAt(Instant.now()); + entity.setFeedback(body.getFeedback()); + + return toFeedback(feedbackRepository.save(entity)); + } + @Transactional(readOnly = true) public Feedback getFeedbackDetails(UUID feedbackId, UUID requesterId, boolean isAdmin) { FeedbackEntity entity = findFeedbackOrThrow(feedbackId); @@ -77,11 +107,72 @@ public Feedback getFeedbackDetails(UUID feedbackId, UUID requesterId, boolean is return toFeedback(entity); } + @Transactional + public Feedback updateFeedbackDetails(UUID feedbackId, FeedbackPartialUpdate body, UUID requesterId, boolean isAdmin) { + FeedbackEntity entity = findFeedbackOrThrow(feedbackId); + if (!isAdmin && !requesterId.equals(entity.getCreatorId())) { + throw new ForbiddenException("Access denied"); + } + + if (body.getEvent() != null) { + UUID eventId = parseUuid(body.getEvent(), "event"); + if (!eventRepository.existsById(eventId)) { + throw new BadRequestException("Event not found: " + eventId); + } + entity.setEventId(eventId); + } + if (body.getMember() != null) { + UUID memberId = parseUuid(body.getMember(), "member"); + if (!memberRepository.existsById(memberId)) { + throw new BadRequestException("Member not found: " + memberId); + } + entity.setMemberId(memberId); + } + if (body.getFeedback() != null) { + entity.setFeedback(body.getFeedback()); + } + + return toFeedback(feedbackRepository.save(entity)); + } + + @Transactional + public void deleteFeedback(UUID feedbackId, UUID requesterId, boolean isAdmin) { + FeedbackEntity entity = findFeedbackOrThrow(feedbackId); + if (!isAdmin && !requesterId.equals(entity.getCreatorId())) { + throw new ForbiddenException("Access denied"); + } + feedbackRepository.delete(entity); + } + + private void assertTrainerOfMember(UUID trainerId, UUID memberId) { + Set trainerTeams = trainerRepository.findAllById_MemberId(trainerId).stream() + .map(t -> t.getId().getTeamId()) + .collect(Collectors.toSet()); + Set memberTeams = traineeRepository.findAllById_MemberId(memberId).stream() + .map(t -> t.getId().getTeamId()) + .collect(Collectors.toSet()); + trainerTeams.retainAll(memberTeams); + if (trainerTeams.isEmpty()) { + throw new ForbiddenException("Access denied"); + } + } + private FeedbackEntity findFeedbackOrThrow(UUID feedbackId) { return feedbackRepository.findById(feedbackId) .orElseThrow(() -> new NotFoundException("Feedback not found: " + feedbackId)); } + private UUID parseUuid(String value, String fieldName) { + if (value == null) { + throw new BadRequestException("Field '" + fieldName + "' is required"); + } + try { + return UUID.fromString(value); + } catch (IllegalArgumentException ex) { + throw new BadRequestException("Invalid UUID for '" + fieldName + "': " + value); + } + } + private Feedback toFeedback(FeedbackEntity entity) { return new Feedback( entity.getId(), From bd3a13f763cc8725b1b0d418936b2406b72156cb Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Fri, 19 Jun 2026 13:45:11 +0200 Subject: [PATCH 5/7] add feedback service tests --- .../FeedbackServiceApplicationTests.java | 10 + .../controller/FeedbackControllerTest.java | 277 ++++++++++++ .../service/FeedbackServiceTest.java | 406 ++++++++++++++++++ 3 files changed, 693 insertions(+) create mode 100644 services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java create mode 100644 services/spring-feedback/src/test/java/tum/devoops/feedbackservice/service/FeedbackServiceTest.java diff --git a/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/FeedbackServiceApplicationTests.java b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/FeedbackServiceApplicationTests.java index d430cc8..c81ed6d 100644 --- a/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/FeedbackServiceApplicationTests.java +++ b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/FeedbackServiceApplicationTests.java @@ -2,7 +2,11 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import tum.devoops.feedbackservice.service.FeedbackService; /** * Context-load smoke test. @@ -20,6 +24,12 @@ }) class FeedbackServiceApplicationTests { + @MockitoBean + private FeedbackService feedbackService; + + @MockitoBean + private JwtDecoder jwtDecoder; + @Test void contextLoads() { } diff --git a/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java new file mode 100644 index 0000000..9fc4fbb --- /dev/null +++ b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java @@ -0,0 +1,277 @@ +package tum.devoops.feedbackservice.controller; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import tum.devoops.feedbackservice.config.SecurityConfig; +import tum.devoops.feedbackservice.exception.BadRequestException; +import tum.devoops.feedbackservice.exception.ForbiddenException; +import tum.devoops.feedbackservice.exception.NotFoundException; +import tum.devoops.feedbackservice.model.Feedback; +import tum.devoops.feedbackservice.model.FeedbackSummary; +import tum.devoops.feedbackservice.service.FeedbackService; + +@WebMvcTest(FeedbackController.class) +@Import(SecurityConfig.class) +class FeedbackControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private FeedbackService feedbackService; + + private static final UUID REQUESTER_ID = UUID.randomUUID(); + private static final UUID FEEDBACK_ID = UUID.randomUUID(); + private static final UUID EVENT_ID = UUID.randomUUID(); + private static final UUID MEMBER_ID = UUID.randomUUID(); + + private Feedback sampleFeedback() { + return new Feedback(FEEDBACK_ID, EVENT_ID.toString(), MEMBER_ID.toString(), + REQUESTER_ID.toString(), OffsetDateTime.now(), "Great work!"); + } + + private FeedbackSummary sampleSummary() { + return new FeedbackSummary(FEEDBACK_ID, EVENT_ID.toString(), MEMBER_ID.toString(), + REQUESTER_ID.toString(), OffsetDateTime.now()); + } + + private String feedbackCreateJson(UUID eventId, UUID memberId, String text) { + return "{\"event\":\"" + eventId + "\",\"member\":\"" + memberId + "\",\"feedback\":\"" + text + "\"}"; + } + + // ─── GET /feedback ──────────────────────────────────────────────────────── + + @Test + void getAllFeedbackWithoutAuthReturns401() throws Exception { + mockMvc.perform(get("/feedback")) + .andExpect(status().isUnauthorized()); + } + + @Test + void getAllFeedbackWithAuthReturns200AndList() throws Exception { + when(feedbackService.getAllFeedback(REQUESTER_ID, false)).thenReturn(List.of(sampleSummary())); + + mockMvc.perform(get("/feedback") + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").exists()) + .andExpect(jsonPath("$[0].event").exists()) + .andExpect(jsonPath("$[0].member").exists()) + .andExpect(jsonPath("$[0].creator").exists()); + } + + @Test + void getAllFeedbackAsAdminPassesIsAdminTrue() throws Exception { + when(feedbackService.getAllFeedback(any(), eq(true))).thenReturn(List.of()); + + mockMvc.perform(get("/feedback") + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())) + .authorities(new SimpleGrantedAuthority("ROLE_admin")))) + .andExpect(status().isOk()); + + verify(feedbackService).getAllFeedback(REQUESTER_ID, true); + } + + // ─── POST /feedback ─────────────────────────────────────────────────────── + + @Test + void createFeedbackWithoutAuthReturns401() throws Exception { + mockMvc.perform(post("/feedback") + .contentType(MediaType.APPLICATION_JSON) + .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "x"))) + .andExpect(status().isUnauthorized()); + } + + @Test + void createFeedbackWithAuthReturns201AndBody() throws Exception { + when(feedbackService.createFeedback(any(), eq(REQUESTER_ID), eq(false))).thenReturn(sampleFeedback()); + + mockMvc.perform(post("/feedback") + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "Great!"))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.event").exists()) + .andExpect(jsonPath("$.member").exists()) + .andExpect(jsonPath("$.creator").exists()) + .andExpect(jsonPath("$.feedback").exists()); + } + + @Test + void createFeedbackServiceThrowsForbiddenReturns403() throws Exception { + when(feedbackService.createFeedback(any(), any(), anyBoolean())) + .thenThrow(new ForbiddenException("Access denied")); + + mockMvc.perform(post("/feedback") + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "x"))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").exists()); + } + + @Test + void createFeedbackServiceThrowsBadRequestReturns400() throws Exception { + when(feedbackService.createFeedback(any(), any(), anyBoolean())) + .thenThrow(new BadRequestException("Event not found")); + + mockMvc.perform(post("/feedback") + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "x"))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").exists()); + } + + // ─── GET /feedback/{id} ─────────────────────────────────────────────────── + + @Test + void getFeedbackDetailsWithoutAuthReturns401() throws Exception { + mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + void getFeedbackDetailsWithAuthReturns200AndBody() throws Exception { + when(feedbackService.getFeedbackDetails(FEEDBACK_ID, REQUESTER_ID, false)).thenReturn(sampleFeedback()); + + mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.feedback").exists()); + } + + @Test + void getFeedbackDetailsServiceThrowsNotFoundReturns404() throws Exception { + when(feedbackService.getFeedbackDetails(any(), any(), anyBoolean())) + .thenThrow(new NotFoundException("Not found")); + + mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").exists()); + } + + @Test + void getFeedbackDetailsServiceThrowsForbiddenReturns403() throws Exception { + when(feedbackService.getFeedbackDetails(any(), any(), anyBoolean())) + .thenThrow(new ForbiddenException("Access denied")); + + mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").exists()); + } + + // ─── PATCH /feedback/{id} ───────────────────────────────────────────────── + + @Test + void updateFeedbackDetailsWithoutAuthReturns401() throws Exception { + mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"feedback\":\"updated\"}")) + .andExpect(status().isUnauthorized()); + } + + @Test + void updateFeedbackDetailsWithAuthReturns200AndBody() throws Exception { + when(feedbackService.updateFeedbackDetails(eq(FEEDBACK_ID), any(), eq(REQUESTER_ID), eq(false))) + .thenReturn(sampleFeedback()); + + mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"feedback\":\"updated\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()); + } + + @Test + void updateFeedbackDetailsServiceThrowsForbiddenReturns403() throws Exception { + when(feedbackService.updateFeedbackDetails(any(), any(), any(), anyBoolean())) + .thenThrow(new ForbiddenException("Access denied")); + + mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"feedback\":\"x\"}")) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").exists()); + } + + @Test + void updateFeedbackDetailsServiceThrowsNotFoundReturns404() throws Exception { + when(feedbackService.updateFeedbackDetails(any(), any(), any(), anyBoolean())) + .thenThrow(new NotFoundException("Not found")); + + mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"feedback\":\"x\"}")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").exists()); + } + + // ─── DELETE /feedback/{id} ──────────────────────────────────────────────── + + @Test + void deleteFeedbackWithoutAuthReturns401() throws Exception { + mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + void deleteFeedbackWithAuthReturns204() throws Exception { + mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteFeedbackServiceThrowsForbiddenReturns403() throws Exception { + doThrow(new ForbiddenException("Access denied")) + .when(feedbackService).deleteFeedback(any(), any(), anyBoolean()); + + mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").exists()); + } + + @Test + void deleteFeedbackServiceThrowsNotFoundReturns404() throws Exception { + doThrow(new NotFoundException("Not found")) + .when(feedbackService).deleteFeedback(any(), any(), anyBoolean()); + + mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID) + .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").exists()); + } +} diff --git a/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/service/FeedbackServiceTest.java b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/service/FeedbackServiceTest.java new file mode 100644 index 0000000..1d8850e --- /dev/null +++ b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/service/FeedbackServiceTest.java @@ -0,0 +1,406 @@ +package tum.devoops.feedbackservice.service; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import static org.mockito.ArgumentMatchers.any; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; + +import tum.devoops.feedbackservice.entity.FeedbackEntity; +import tum.devoops.feedbackservice.entity.TraineeEntity; +import tum.devoops.feedbackservice.entity.TrainerEntity; +import tum.devoops.feedbackservice.exception.BadRequestException; +import tum.devoops.feedbackservice.exception.ForbiddenException; +import tum.devoops.feedbackservice.exception.NotFoundException; +import tum.devoops.feedbackservice.model.Feedback; +import tum.devoops.feedbackservice.model.FeedbackCreate; +import tum.devoops.feedbackservice.model.FeedbackPartialUpdate; +import tum.devoops.feedbackservice.model.FeedbackSummary; +import tum.devoops.feedbackservice.repository.EventRepository; +import tum.devoops.feedbackservice.repository.FeedbackRepository; +import tum.devoops.feedbackservice.repository.MemberRepository; +import tum.devoops.feedbackservice.repository.TraineeRepository; +import tum.devoops.feedbackservice.repository.TrainerRepository; + +@ExtendWith(MockitoExtension.class) +class FeedbackServiceTest { + + @Mock + private FeedbackRepository feedbackRepository; + @Mock + private EventRepository eventRepository; + @Mock + private MemberRepository memberRepository; + @Mock + private TrainerRepository trainerRepository; + @Mock + private TraineeRepository traineeRepository; + + @InjectMocks + private FeedbackService service; + + private static final UUID REQUESTER_ID = UUID.randomUUID(); + private static final UUID ANOTHER_ID = UUID.randomUUID(); + private static final UUID EVENT_ID = UUID.randomUUID(); + private static final UUID MEMBER_ID = UUID.randomUUID(); + private static final UUID FEEDBACK_ID = UUID.randomUUID(); + private static final UUID TEAM_ID = UUID.randomUUID(); + + private FeedbackEntity makeEntity(UUID id, UUID eventId, UUID memberId, UUID creatorId) { + FeedbackEntity e = new FeedbackEntity(); + e.setId(id); + e.setEventId(eventId); + e.setMemberId(memberId); + e.setCreatorId(creatorId); + e.setCreatedAt(Instant.now()); + e.setFeedback("test feedback"); + return e; + } + + private void stubExistingEventAndMember() { + when(eventRepository.existsById(EVENT_ID)).thenReturn(true); + when(memberRepository.existsById(MEMBER_ID)).thenReturn(true); + } + + private void stubTrainerOfMember() { + when(trainerRepository.findAllById_MemberId(REQUESTER_ID)) + .thenReturn(List.of(new TrainerEntity(new TrainerEntity.Id(TEAM_ID, REQUESTER_ID)))); + when(traineeRepository.findAllById_MemberId(MEMBER_ID)) + .thenReturn(List.of(new TraineeEntity(new TraineeEntity.Id(TEAM_ID, MEMBER_ID)))); + } + + // ─── getAllFeedback ──────────────────────────────────────────────────────── + + @Test + void getAllFeedbackAsAdminReturnsAll() { + FeedbackEntity e1 = makeEntity(UUID.randomUUID(), EVENT_ID, MEMBER_ID, REQUESTER_ID); + FeedbackEntity e2 = makeEntity(UUID.randomUUID(), EVENT_ID, ANOTHER_ID, ANOTHER_ID); + when(feedbackRepository.findAll()).thenReturn(List.of(e1, e2)); + + List result = service.getAllFeedback(REQUESTER_ID, true); + + assertThat(result).hasSize(2); + } + + @Test + void getAllFeedbackAsNonAdminReturnsOwnFeedback() { + FeedbackEntity asCreator = makeEntity(UUID.randomUUID(), EVENT_ID, MEMBER_ID, REQUESTER_ID); + FeedbackEntity asMember = makeEntity(UUID.randomUUID(), EVENT_ID, REQUESTER_ID, ANOTHER_ID); + when(feedbackRepository.findAllByCreatorId(REQUESTER_ID)).thenReturn(List.of(asCreator)); + when(feedbackRepository.findAllByMemberId(REQUESTER_ID)).thenReturn(List.of(asMember)); + + List result = service.getAllFeedback(REQUESTER_ID, false); + + assertThat(result).hasSize(2); + assertThat(result).extracting(FeedbackSummary::getCreator) + .contains(REQUESTER_ID.toString(), ANOTHER_ID.toString()); + } + + @Test + void getAllFeedbackDeduplicatesOverlappingEntries() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, REQUESTER_ID, REQUESTER_ID); + when(feedbackRepository.findAllByCreatorId(REQUESTER_ID)).thenReturn(List.of(e)); + when(feedbackRepository.findAllByMemberId(REQUESTER_ID)).thenReturn(List.of(e)); + + List result = service.getAllFeedback(REQUESTER_ID, false); + + assertThat(result).hasSize(1); + } + + @Test + void getAllFeedbackReturnsEmptyForNoFeedback() { + when(feedbackRepository.findAllByCreatorId(REQUESTER_ID)).thenReturn(List.of()); + when(feedbackRepository.findAllByMemberId(REQUESTER_ID)).thenReturn(List.of()); + + List result = service.getAllFeedback(REQUESTER_ID, false); + + assertThat(result).isEmpty(); + } + + // ─── createFeedback ─────────────────────────────────────────────────────── + + @Test + void createFeedbackAsAdminSkipsTrainerCheck() { + stubExistingEventAndMember(); + FeedbackEntity saved = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.save(any())).thenReturn(saved); + FeedbackCreate body = new FeedbackCreate(EVENT_ID.toString(), MEMBER_ID.toString(), "Great work!"); + + Feedback result = service.createFeedback(body, REQUESTER_ID, true); + + assertThat(result.getId()).isEqualTo(FEEDBACK_ID); + assertThat(result.getCreator()).isEqualTo(REQUESTER_ID.toString()); + } + + @Test + void createFeedbackAsTrainerWithSharedTeamSucceeds() { + stubExistingEventAndMember(); + stubTrainerOfMember(); + FeedbackEntity saved = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.save(any())).thenReturn(saved); + FeedbackCreate body = new FeedbackCreate(EVENT_ID.toString(), MEMBER_ID.toString(), "Keep it up!"); + + Feedback result = service.createFeedback(body, REQUESTER_ID, false); + + assertThat(result.getMember()).isEqualTo(MEMBER_ID.toString()); + } + + @Test + void createFeedbackAsTrainerWithoutSharedTeamThrowsForbidden() { + stubExistingEventAndMember(); + UUID otherTeam = UUID.randomUUID(); + when(trainerRepository.findAllById_MemberId(REQUESTER_ID)) + .thenReturn(List.of(new TrainerEntity(new TrainerEntity.Id(TEAM_ID, REQUESTER_ID)))); + when(traineeRepository.findAllById_MemberId(MEMBER_ID)) + .thenReturn(List.of(new TraineeEntity(new TraineeEntity.Id(otherTeam, MEMBER_ID)))); + FeedbackCreate body = new FeedbackCreate(EVENT_ID.toString(), MEMBER_ID.toString(), "x"); + + assertThatThrownBy(() -> service.createFeedback(body, REQUESTER_ID, false)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + void createFeedbackAsTrainerWithNoTeamsThrowsForbidden() { + stubExistingEventAndMember(); + when(trainerRepository.findAllById_MemberId(REQUESTER_ID)).thenReturn(List.of()); + when(traineeRepository.findAllById_MemberId(MEMBER_ID)).thenReturn(List.of()); + FeedbackCreate body = new FeedbackCreate(EVENT_ID.toString(), MEMBER_ID.toString(), "x"); + + assertThatThrownBy(() -> service.createFeedback(body, REQUESTER_ID, false)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + void createFeedbackWithNonExistentEventThrowsBadRequest() { + when(eventRepository.existsById(EVENT_ID)).thenReturn(false); + FeedbackCreate body = new FeedbackCreate(EVENT_ID.toString(), MEMBER_ID.toString(), "x"); + + assertThatThrownBy(() -> service.createFeedback(body, REQUESTER_ID, true)) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("Event"); + } + + @Test + void createFeedbackWithNonExistentMemberThrowsBadRequest() { + when(eventRepository.existsById(EVENT_ID)).thenReturn(true); + when(memberRepository.existsById(MEMBER_ID)).thenReturn(false); + FeedbackCreate body = new FeedbackCreate(EVENT_ID.toString(), MEMBER_ID.toString(), "x"); + + assertThatThrownBy(() -> service.createFeedback(body, REQUESTER_ID, true)) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("Member"); + } + + @Test + void createFeedbackWithInvalidEventUuidThrowsBadRequest() { + FeedbackCreate body = new FeedbackCreate("not-a-uuid", MEMBER_ID.toString(), "x"); + + assertThatThrownBy(() -> service.createFeedback(body, REQUESTER_ID, true)) + .isInstanceOf(BadRequestException.class); + } + + @Test + void createFeedbackWithInvalidMemberUuidThrowsBadRequest() { + FeedbackCreate body = new FeedbackCreate(EVENT_ID.toString(), "not-a-uuid", "x"); + + assertThatThrownBy(() -> service.createFeedback(body, REQUESTER_ID, true)) + .isInstanceOf(BadRequestException.class); + } + + // ─── getFeedbackDetails ─────────────────────────────────────────────────── + + @Test + void getFeedbackDetailsAsAdminReturnsAnyFeedback() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, ANOTHER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + Feedback result = service.getFeedbackDetails(FEEDBACK_ID, REQUESTER_ID, true); + + assertThat(result.getId()).isEqualTo(FEEDBACK_ID); + } + + @Test + void getFeedbackDetailsAsCreatorSucceeds() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + Feedback result = service.getFeedbackDetails(FEEDBACK_ID, REQUESTER_ID, false); + + assertThat(result.getCreator()).isEqualTo(REQUESTER_ID.toString()); + } + + @Test + void getFeedbackDetailsAsMemberSubjectSucceeds() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, REQUESTER_ID, ANOTHER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + Feedback result = service.getFeedbackDetails(FEEDBACK_ID, REQUESTER_ID, false); + + assertThat(result.getMember()).isEqualTo(REQUESTER_ID.toString()); + } + + @Test + void getFeedbackDetailsAsUnrelatedUserThrowsForbidden() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, ANOTHER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + assertThatThrownBy(() -> service.getFeedbackDetails(FEEDBACK_ID, REQUESTER_ID, false)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + void getFeedbackDetailsNotFoundThrowsNotFoundException() { + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> service.getFeedbackDetails(FEEDBACK_ID, REQUESTER_ID, false)) + .isInstanceOf(NotFoundException.class); + } + + // ─── updateFeedbackDetails ──────────────────────────────────────────────── + + @Test + void updateFeedbackDetailsAsAdminSucceeds() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, ANOTHER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + when(feedbackRepository.save(any())).thenReturn(e); + + Feedback result = service.updateFeedbackDetails( + FEEDBACK_ID, new FeedbackPartialUpdate().feedback("updated"), REQUESTER_ID, true); + + assertThat(result).isNotNull(); + } + + @Test + void updateFeedbackDetailsAsCreatorSucceeds() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + when(feedbackRepository.save(any())).thenReturn(e); + + Feedback result = service.updateFeedbackDetails( + FEEDBACK_ID, new FeedbackPartialUpdate().feedback("updated"), REQUESTER_ID, false); + + assertThat(result).isNotNull(); + } + + @Test + void updateFeedbackDetailsAsNonCreatorThrowsForbidden() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, ANOTHER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + assertThatThrownBy(() -> service.updateFeedbackDetails( + FEEDBACK_ID, new FeedbackPartialUpdate().feedback("x"), REQUESTER_ID, false)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + void updateFeedbackDetailsNotFoundThrowsNotFoundException() { + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> service.updateFeedbackDetails( + FEEDBACK_ID, new FeedbackPartialUpdate(), REQUESTER_ID, true)) + .isInstanceOf(NotFoundException.class); + } + + @Test + void updateFeedbackDetailsWithAllNullFieldsDoesNotModifyEntity() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + ArgumentCaptor captor = ArgumentCaptor.forClass(FeedbackEntity.class); + when(feedbackRepository.save(captor.capture())).thenReturn(e); + + service.updateFeedbackDetails(FEEDBACK_ID, new FeedbackPartialUpdate(), REQUESTER_ID, false); + + assertThat(captor.getValue().getFeedback()).isEqualTo("test feedback"); + assertThat(captor.getValue().getEventId()).isEqualTo(EVENT_ID); + assertThat(captor.getValue().getMemberId()).isEqualTo(MEMBER_ID); + } + + @Test + void updateFeedbackDetailsWithNewEventUpdatesEventId() { + UUID newEventId = UUID.randomUUID(); + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + when(eventRepository.existsById(newEventId)).thenReturn(true); + ArgumentCaptor captor = ArgumentCaptor.forClass(FeedbackEntity.class); + when(feedbackRepository.save(captor.capture())).thenReturn(e); + + service.updateFeedbackDetails( + FEEDBACK_ID, new FeedbackPartialUpdate().event(newEventId.toString()), REQUESTER_ID, false); + + assertThat(captor.getValue().getEventId()).isEqualTo(newEventId); + } + + @Test + void updateFeedbackDetailsWithNonExistentEventThrowsBadRequest() { + UUID newEventId = UUID.randomUUID(); + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + when(eventRepository.existsById(newEventId)).thenReturn(false); + + assertThatThrownBy(() -> service.updateFeedbackDetails( + FEEDBACK_ID, new FeedbackPartialUpdate().event(newEventId.toString()), REQUESTER_ID, false)) + .isInstanceOf(BadRequestException.class); + } + + @Test + void updateFeedbackDetailsWithNonExistentMemberThrowsBadRequest() { + UUID newMemberId = UUID.randomUUID(); + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + when(memberRepository.existsById(newMemberId)).thenReturn(false); + + assertThatThrownBy(() -> service.updateFeedbackDetails( + FEEDBACK_ID, new FeedbackPartialUpdate().member(newMemberId.toString()), REQUESTER_ID, false)) + .isInstanceOf(BadRequestException.class); + } + + // ─── deleteFeedback ─────────────────────────────────────────────────────── + + @Test + void deleteFeedbackAsAdminSucceeds() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, ANOTHER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + service.deleteFeedback(FEEDBACK_ID, REQUESTER_ID, true); + + verify(feedbackRepository).delete(e); + } + + @Test + void deleteFeedbackAsCreatorSucceeds() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, REQUESTER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + service.deleteFeedback(FEEDBACK_ID, REQUESTER_ID, false); + + verify(feedbackRepository).delete(e); + } + + @Test + void deleteFeedbackAsNonCreatorThrowsForbidden() { + FeedbackEntity e = makeEntity(FEEDBACK_ID, EVENT_ID, MEMBER_ID, ANOTHER_ID); + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.of(e)); + + assertThatThrownBy(() -> service.deleteFeedback(FEEDBACK_ID, REQUESTER_ID, false)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + void deleteFeedbackNotFoundThrowsNotFoundException() { + when(feedbackRepository.findById(FEEDBACK_ID)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> service.deleteFeedback(FEEDBACK_ID, REQUESTER_ID, false)) + .isInstanceOf(NotFoundException.class); + } +} From f212abaa0ce2868430fb7b323f8c5fc2ff3d2a0b Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Fri, 19 Jun 2026 15:22:53 +0200 Subject: [PATCH 6/7] add pre-authorization annotation to endpoints --- .../config/SecurityConfig.java | 12 +-- .../controller/FeedbackController.java | 2 + .../controller/FeedbackControllerTest.java | 78 +++++++++++++++---- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/config/SecurityConfig.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/config/SecurityConfig.java index a62a1d2..0aa40c9 100644 --- a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/config/SecurityConfig.java +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/config/SecurityConfig.java @@ -1,5 +1,10 @@ package tum.devoops.feedbackservice.config; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -10,14 +15,9 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - @Configuration @EnableWebSecurity -@EnableMethodSecurity +@EnableMethodSecurity(proxyTargetClass = true) public class SecurityConfig { @Bean diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java index 701714e..314cae2 100644 --- a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/controller/FeedbackController.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; @@ -18,6 +19,7 @@ import tum.devoops.feedbackservice.service.FeedbackService; @RestController +@PreAuthorize("hasAnyRole('admin', 'member')") public class FeedbackController implements FeedbackApi { private final FeedbackService feedbackService; diff --git a/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java index 9fc4fbb..1f5cf36 100644 --- a/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java +++ b/services/spring-feedback/src/test/java/tum/devoops/feedbackservice/controller/FeedbackControllerTest.java @@ -23,6 +23,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import org.springframework.test.web.servlet.request.RequestPostProcessor; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -63,6 +64,16 @@ private String feedbackCreateJson(UUID eventId, UUID memberId, String text) { return "{\"event\":\"" + eventId + "\",\"member\":\"" + memberId + "\",\"feedback\":\"" + text + "\"}"; } + private static RequestPostProcessor memberJwt() { + return jwt().jwt(j -> j.subject(REQUESTER_ID.toString())) + .authorities(new SimpleGrantedAuthority("ROLE_member")); + } + + private static RequestPostProcessor trainerJwt() { + return jwt().jwt(j -> j.subject(REQUESTER_ID.toString())) + .authorities(new SimpleGrantedAuthority("ROLE_trainer")); + } + // ─── GET /feedback ──────────────────────────────────────────────────────── @Test @@ -76,7 +87,7 @@ void getAllFeedbackWithAuthReturns200AndList() throws Exception { when(feedbackService.getAllFeedback(REQUESTER_ID, false)).thenReturn(List.of(sampleSummary())); mockMvc.perform(get("/feedback") - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .with(memberJwt())) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].id").exists()) .andExpect(jsonPath("$[0].event").exists()) @@ -111,7 +122,7 @@ void createFeedbackWithAuthReturns201AndBody() throws Exception { when(feedbackService.createFeedback(any(), eq(REQUESTER_ID), eq(false))).thenReturn(sampleFeedback()); mockMvc.perform(post("/feedback") - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .with(memberJwt()) .contentType(MediaType.APPLICATION_JSON) .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "Great!"))) .andExpect(status().isCreated()) @@ -128,7 +139,7 @@ void createFeedbackServiceThrowsForbiddenReturns403() throws Exception { .thenThrow(new ForbiddenException("Access denied")); mockMvc.perform(post("/feedback") - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .with(memberJwt()) .contentType(MediaType.APPLICATION_JSON) .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "x"))) .andExpect(status().isForbidden()) @@ -141,7 +152,7 @@ void createFeedbackServiceThrowsBadRequestReturns400() throws Exception { .thenThrow(new BadRequestException("Event not found")); mockMvc.perform(post("/feedback") - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .with(memberJwt()) .contentType(MediaType.APPLICATION_JSON) .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "x"))) .andExpect(status().isBadRequest()) @@ -161,7 +172,7 @@ void getFeedbackDetailsWithAuthReturns200AndBody() throws Exception { when(feedbackService.getFeedbackDetails(FEEDBACK_ID, REQUESTER_ID, false)).thenReturn(sampleFeedback()); mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .with(memberJwt())) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").exists()) .andExpect(jsonPath("$.feedback").exists()); @@ -173,7 +184,7 @@ void getFeedbackDetailsServiceThrowsNotFoundReturns404() throws Exception { .thenThrow(new NotFoundException("Not found")); mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .with(memberJwt())) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").exists()); } @@ -184,7 +195,7 @@ void getFeedbackDetailsServiceThrowsForbiddenReturns403() throws Exception { .thenThrow(new ForbiddenException("Access denied")); mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .with(memberJwt())) .andExpect(status().isForbidden()) .andExpect(jsonPath("$.message").exists()); } @@ -205,7 +216,7 @@ void updateFeedbackDetailsWithAuthReturns200AndBody() throws Exception { .thenReturn(sampleFeedback()); mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .with(memberJwt()) .contentType(MediaType.APPLICATION_JSON) .content("{\"feedback\":\"updated\"}")) .andExpect(status().isOk()) @@ -218,7 +229,7 @@ void updateFeedbackDetailsServiceThrowsForbiddenReturns403() throws Exception { .thenThrow(new ForbiddenException("Access denied")); mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .with(memberJwt()) .contentType(MediaType.APPLICATION_JSON) .content("{\"feedback\":\"x\"}")) .andExpect(status().isForbidden()) @@ -231,7 +242,7 @@ void updateFeedbackDetailsServiceThrowsNotFoundReturns404() throws Exception { .thenThrow(new NotFoundException("Not found")); mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString()))) + .with(memberJwt()) .contentType(MediaType.APPLICATION_JSON) .content("{\"feedback\":\"x\"}")) .andExpect(status().isNotFound()) @@ -249,7 +260,7 @@ void deleteFeedbackWithoutAuthReturns401() throws Exception { @Test void deleteFeedbackWithAuthReturns204() throws Exception { mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .with(memberJwt())) .andExpect(status().isNoContent()); } @@ -259,7 +270,7 @@ void deleteFeedbackServiceThrowsForbiddenReturns403() throws Exception { .when(feedbackService).deleteFeedback(any(), any(), anyBoolean()); mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .with(memberJwt())) .andExpect(status().isForbidden()) .andExpect(jsonPath("$.message").exists()); } @@ -270,8 +281,49 @@ void deleteFeedbackServiceThrowsNotFoundReturns404() throws Exception { .when(feedbackService).deleteFeedback(any(), any(), anyBoolean()); mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID) - .with(jwt().jwt(j -> j.subject(REQUESTER_ID.toString())))) + .with(memberJwt())) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").exists()); } + + // ─── @PreAuthorize role checks ──────────────────────────────────────────── + + @Test + void getAllFeedbackWithWrongRoleReturns403() throws Exception { + mockMvc.perform(get("/feedback") + .with(trainerJwt())) + .andExpect(status().isForbidden()); + } + + @Test + void createFeedbackWithWrongRoleReturns403() throws Exception { + mockMvc.perform(post("/feedback") + .with(trainerJwt()) + .contentType(MediaType.APPLICATION_JSON) + .content(feedbackCreateJson(EVENT_ID, MEMBER_ID, "x"))) + .andExpect(status().isForbidden()); + } + + @Test + void getFeedbackDetailsWithWrongRoleReturns403() throws Exception { + mockMvc.perform(get("/feedback/{id}", FEEDBACK_ID) + .with(trainerJwt())) + .andExpect(status().isForbidden()); + } + + @Test + void updateFeedbackDetailsWithWrongRoleReturns403() throws Exception { + mockMvc.perform(patch("/feedback/{id}", FEEDBACK_ID) + .with(trainerJwt()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"feedback\":\"x\"}")) + .andExpect(status().isForbidden()); + } + + @Test + void deleteFeedbackWithWrongRoleReturns403() throws Exception { + mockMvc.perform(delete("/feedback/{id}", FEEDBACK_ID) + .with(trainerJwt())) + .andExpect(status().isForbidden()); + } } From b341938078dd8968284b1d8707a7e459b61dbe68 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Fri, 19 Jun 2026 16:09:12 +0200 Subject: [PATCH 7/7] changed service constructor to autowired annotation --- .../service/FeedbackService.java | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java index d5b3f8e..1405e2a 100644 --- a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java +++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/service/FeedbackService.java @@ -9,6 +9,7 @@ import java.util.UUID; import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,24 +30,16 @@ @Service public class FeedbackService { - private final FeedbackRepository feedbackRepository; - private final EventRepository eventRepository; - private final MemberRepository memberRepository; - private final TrainerRepository trainerRepository; - private final TraineeRepository traineeRepository; - - public FeedbackService( - FeedbackRepository feedbackRepository, - EventRepository eventRepository, - MemberRepository memberRepository, - TrainerRepository trainerRepository, - TraineeRepository traineeRepository) { - this.feedbackRepository = feedbackRepository; - this.eventRepository = eventRepository; - this.memberRepository = memberRepository; - this.trainerRepository = trainerRepository; - this.traineeRepository = traineeRepository; - } + @Autowired + private FeedbackRepository feedbackRepository; + @Autowired + private EventRepository eventRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private TrainerRepository trainerRepository; + @Autowired + private TraineeRepository traineeRepository; @Transactional(readOnly = true) public List getAllFeedback(UUID requesterId, boolean isAdmin) {