From d874977222ef2386c42f101bf02eb071044f140d Mon Sep 17 00:00:00 2001 From: Alexander Michael Wudy Date: Thu, 9 Jul 2026 00:40:48 +0200 Subject: [PATCH 1/2] implements #54: implements note service --- api/common.yaml | 13 + api/note-service.yaml | 118 ++++- services/note-service/pom.xml | 22 + .../de/tum/devopss26/noteservice/Note.java | 27 -- .../controller/NoteController.java | 70 +-- .../devopss26/noteservice/entity/Note.java | 39 ++ .../exception/IllegalNoteAccessException.java | 20 + .../exception/NoteNotFoundException.java | 11 + .../noteservice/mapper/NoteMapper.java | 31 ++ .../repository/NoteRepository.java | 12 + .../noteservice/service/NoteService.java | 20 + .../noteservice/service/NoteServiceImpl.java | 95 ++++ ....changelog-v1-create-initial-structure.xml | 30 ++ .../controller/NoteControllerTest.java | 429 ++++++++++++++++++ web-client/src/common.ts | 10 + 15 files changed, 863 insertions(+), 84 deletions(-) delete mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/Note.java create mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java create mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java create mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/NoteNotFoundException.java create mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java create mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/repository/NoteRepository.java create mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteService.java create mode 100644 services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java create mode 100644 services/note-service/src/test/java/de/tum/devopss26/noteservice/controller/NoteControllerTest.java diff --git a/api/common.yaml b/api/common.yaml index 6f7507a..5254e58 100644 --- a/api/common.yaml +++ b/api/common.yaml @@ -30,6 +30,19 @@ components: format: int64 required: - id + Timestamped: + type: object + additionalProperties: true + properties: + createdAt: + type: string + format: date-time + lastUpdatedAt: + type: string + format: date-time + required: + - createdAt + - lastUpdatedAt securitySchemes: # Only intended to be used for loginUser, as every other authorized endpoint should require a JWT BasicAuth: diff --git a/api/note-service.yaml b/api/note-service.yaml index 7457492..2ae3f1f 100644 --- a/api/note-service.yaml +++ b/api/note-service.yaml @@ -11,52 +11,56 @@ servers: - url: / paths: - /api/v1/notes: get: tags: [Notes] summary: Get all notes for a user operationId: getNotes - parameters: - - name: userId - in: query - required: true - schema: - type: integer - format: int64 + security: + - JWTAuth: [] responses: '200': description: List of notes content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Note' + $ref: '#/components/schemas/ListNotesResponse' + '401': + description: Token either does not exist or is already expired + '500': + description: Unhandled error occurred post: tags: [Notes] summary: Create a new note operationId: createNote + security: + - JWTAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/Note' + $ref: '#/components/schemas/CreateNoteRequest' responses: '201': description: Note created content: application/json: schema: - $ref: '#/components/schemas/Note' + $ref: '#/components/schemas/CreateNoteResponse' + '401': + description: Token either does not exist or is already expired + '500': + description: Unhandled error occurred /api/v1/notes/{id}: get: tags: [Notes] summary: Get a note by ID operationId: getNoteById + security: + - JWTAuth: [] parameters: - $ref: '#/components/parameters/id' responses: @@ -65,14 +69,22 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Note' + $ref: '#/components/schemas/GetNoteResponse' + '401': + description: Token either does not exist or is already expired + '403': + description: Accessing note of someone else '404': description: Note not found + '500': + description: Unhandled error occurred put: tags: [Notes] summary: Update a note's title and content operationId: updateNote + security: + - JWTAuth: [] parameters: - $ref: '#/components/parameters/id' requestBody: @@ -87,24 +99,40 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Note' + $ref: '#/components/schemas/UpdateNoteResponse' + '401': + description: Token either does not exist or is already expired + '403': + description: Accessing note of someone else '404': description: Note not found + '500': + description: Unhandled error occurred delete: tags: [Notes] summary: Delete a note by ID operationId: deleteNote + security: + - JWTAuth: [] parameters: - $ref: '#/components/parameters/id' responses: '204': description: Note deleted + '401': + description: Token either does not exist or is already expired + '403': + description: Accessing note of someone else '404': description: Note not found + '500': + description: Unhandled error occurred components: - + securitySchemes: + JWTAuth: + $ref: './common.yaml#/components/securitySchemes/JWTAuth' parameters: id: name: id @@ -117,17 +145,57 @@ components: schemas: Note: type: object + additionalProperties: true properties: - id: - type: integer - format: int64 title: type: string content: type: string - createdAt: - type: string - format: date-time - lastUpdatedAt: - type: string - format: date-time + ListNotesResponse: + type: object + properties: + notes: + type: array + items: + $ref: "#/components/schemas/IdentifiedTimestampedNote" + required: + - notes + CreateNoteRequest: + allOf: + - $ref: '#/components/schemas/Note' + - type: object + required: + - title + - content + CreateNoteResponse: + allOf: + - $ref: '#/components/schemas/IdentifiedTimestampedNote' + - type: object + GetNoteResponse: + allOf: + - $ref: "#/components/schemas/IdentifiedTimestampedNote" + - type: object + UpdateNoteResponse: + allOf: + - $ref: "#/components/schemas/IdentifiedTimestampedNote" + - type: object + IdentifiedNote: + type: object + allOf: + - $ref: './common.yaml#/components/schemas/Identifier' + - $ref: "#/components/schemas/Note" + required: + - id + - title + - content + IdentifiedTimestampedNote: + type: object + allOf: + - $ref: './common.yaml#/components/schemas/Timestamped' + - $ref: "#/components/schemas/IdentifiedNote" + required: + - id + - title + - content + - createdAt + - lastUpdatedAt diff --git a/services/note-service/pom.xml b/services/note-service/pom.xml index 179e0fb..69f4d4c 100644 --- a/services/note-service/pom.xml +++ b/services/note-service/pom.xml @@ -66,6 +66,11 @@ micrometer-registry-prometheus runtime + + org.mapstruct + mapstruct + 1.5.5.Final + org.springframework.boot spring-boot-starter-data-jpa-test @@ -91,6 +96,12 @@ spring-boot-testcontainers test + + com.atlassian.oai + openapi-request-validator-mockmvc + 3.0.0 + test + org.testcontainers postgresql @@ -116,6 +127,7 @@ ${project.basedir}/../../api note-service.yaml + common.yaml @@ -145,6 +157,16 @@ org.projectlombok lombok + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/Note.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/Note.java deleted file mode 100644 index 3267e07..0000000 --- a/services/note-service/src/main/java/de/tum/devopss26/noteservice/Note.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.tum.devopss26.noteservice; - -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Getter -@Setter -public class Note { - - private Long id; - private String title; - private String content; - private LocalDateTime createdAt; - private LocalDateTime lastUpdatedAt; - - public Note() {} - - public Note(Long id, String title, String content) { - this.id = id; - this.title = title; - this.content = content; - this.createdAt = LocalDateTime.now(); - this.lastUpdatedAt = LocalDateTime.now(); - } -} diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/controller/NoteController.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/controller/NoteController.java index 48a3531..dd0eb3d 100644 --- a/services/note-service/src/main/java/de/tum/devopss26/noteservice/controller/NoteController.java +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/controller/NoteController.java @@ -1,59 +1,65 @@ package de.tum.devopss26.noteservice.controller; +import de.tum.devopss26.noteservice.service.NoteService; +import de.tum.devopss26.shared.security.JWTHelper; +import de.tum.devopss26.shared.security.RequireTokenValidation; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.openapitools.api.NotesApi; -import org.openapitools.model.Note; +import org.openapitools.model.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController +@RequiredArgsConstructor public class NoteController implements NotesApi { + private final HttpServletRequest servletRequest; + private final NoteService service; + + @RequireTokenValidation @Override - public ResponseEntity> getNotes(Long userId) { - Note note1 = new Note(); - note1.setId(1L); - note1.setTitle("Car Service"); - note1.setContent("Need to take the car in for an oil change. Also ask them to check the tire pressure and brake pads."); - - Note note2 = new Note(); - note2.setId(2L); - note2.setTitle("Post Office"); - note2.setContent("Send the birthday package to aunt Maria. Remember to use express shipping so it arrives before Saturday."); - - Note note3 = new Note(); - note3.setId(3L); - note3.setTitle("Pharmacy"); - note3.setContent("Pick up prescription for blood pressure medication. Also grab some vitamin D supplements and a thermometer."); - - return ResponseEntity.ok(List.of(note1, note2, note3)); + public ResponseEntity getNotes() { + long userId = JWTHelper.extractFrom(servletRequest).getUserId(); + + ListNotesResponse response = service.getNotes(userId); + return ResponseEntity.ok(response); } + @RequireTokenValidation @Override - public ResponseEntity getNoteById(Long id) { - Note note = new Note(); - note.setId(id); - note.setTitle("Sample Note"); - note.setContent("Sample content"); - return ResponseEntity.ok(note); + public ResponseEntity getNoteById(Long id) { + long userId = JWTHelper.extractFrom(servletRequest).getUserId(); + + GetNoteResponse response = service.getNote(userId, id); + return ResponseEntity.ok(response); } + @RequireTokenValidation @Override - public ResponseEntity createNote(Note note) { - note.setId(1L); - return ResponseEntity.status(HttpStatus.CREATED).body(note); + public ResponseEntity createNote(CreateNoteRequest request) { + long userId = JWTHelper.extractFrom(servletRequest).getUserId(); + + CreateNoteResponse response = service.createNote(request, userId); + return ResponseEntity.status(HttpStatus.CREATED).body(response); } + @RequireTokenValidation @Override - public ResponseEntity updateNote(Long id, Note note) { - note.setId(id); - return ResponseEntity.ok(note); + public ResponseEntity updateNote(Long id, Note note) { + long userId = JWTHelper.extractFrom(servletRequest).getUserId(); + + UpdateNoteResponse response = service.updateNote(userId, id, note); + return ResponseEntity.ok(response); } + @RequireTokenValidation @Override public ResponseEntity deleteNote(Long id) { + long userId = JWTHelper.extractFrom(servletRequest).getUserId(); + + service.deleteNote(userId, id); return ResponseEntity.noContent().build(); } } diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java new file mode 100644 index 0000000..86433c4 --- /dev/null +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java @@ -0,0 +1,39 @@ +package de.tum.devopss26.noteservice.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@Entity +@Table(name = "note") +@NoArgsConstructor +@AllArgsConstructor +public class Note { + + @Id + @Column(nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + @Column(name = "last_updated_at", nullable = false) + private OffsetDateTime lastUpdatedAt; + +} diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java new file mode 100644 index 0000000..3f3e51d --- /dev/null +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java @@ -0,0 +1,20 @@ +package de.tum.devopss26.noteservice.exception; + +import de.tum.devopss26.shared.exception.ForbiddenException; +import org.jspecify.annotations.NonNull; + +public class IllegalNoteAccessException extends ForbiddenException { + + public IllegalNoteAccessException(long accessorId, IllegalAccessPair pair) { + super(accessorId + " tried to illegally access note " + pair.noteId + " of owner " + pair.ownerId); + } + + public record IllegalAccessPair(long ownerId, long noteId) { + + @Override + public @NonNull String toString() { + return "(" + ownerId + ", " + noteId + ")"; + } + } + +} diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/NoteNotFoundException.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/NoteNotFoundException.java new file mode 100644 index 0000000..86b00fa --- /dev/null +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/NoteNotFoundException.java @@ -0,0 +1,11 @@ +package de.tum.devopss26.noteservice.exception; + +import de.tum.devopss26.shared.exception.NotFoundException; + +public class NoteNotFoundException extends NotFoundException { + + public NoteNotFoundException(long noteId) { + super("Note with id " + noteId + " not found"); + } + +} diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java new file mode 100644 index 0000000..0c84043 --- /dev/null +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java @@ -0,0 +1,31 @@ +package de.tum.devopss26.noteservice.mapper; + +import de.tum.devopss26.noteservice.entity.Note; +import org.mapstruct.Mapper; +import org.openapitools.model.*; + +import java.util.List; +import java.util.Objects; + +@Mapper(componentModel = "spring") +public interface NoteMapper { + + // General mappers + + IdentifiedTimestampedNote toIdentifiedTimestamped(Note note); + + // Specific mappers + + Note toNote(CreateNoteRequest request, long userId); + + CreateNoteResponse toCreateResponse(Note note); + + GetNoteResponse toGetResponse(Note note); + + default ListNotesResponse toListResponse(List notes) { + return new ListNotesResponse().notes(Objects.requireNonNullElseGet(notes, List::of)); + } + + UpdateNoteResponse toUpdateResponse(Note note); + +} diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/repository/NoteRepository.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/repository/NoteRepository.java new file mode 100644 index 0000000..bda3fba --- /dev/null +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/repository/NoteRepository.java @@ -0,0 +1,12 @@ +package de.tum.devopss26.noteservice.repository; + +import de.tum.devopss26.noteservice.entity.Note; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface NoteRepository extends JpaRepository { + + List findAllByUserId(long userId); + +} diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteService.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteService.java new file mode 100644 index 0000000..8251455 --- /dev/null +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteService.java @@ -0,0 +1,20 @@ +package de.tum.devopss26.noteservice.service; + +import org.openapitools.model.CreateNoteRequest; +import org.openapitools.model.CreateNoteResponse; +import org.openapitools.model.GetNoteResponse; +import org.openapitools.model.ListNotesResponse; +import org.openapitools.model.UpdateNoteResponse; + +public interface NoteService { + + CreateNoteResponse createNote(CreateNoteRequest request, long userId); + + ListNotesResponse getNotes(long userId); + + GetNoteResponse getNote(long userId, long id); + + UpdateNoteResponse updateNote(long userId, long id, org.openapitools.model.Note diff); + + void deleteNote(long userId, long id); +} diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java new file mode 100644 index 0000000..160f240 --- /dev/null +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java @@ -0,0 +1,95 @@ +package de.tum.devopss26.noteservice.service; + +import de.tum.devopss26.noteservice.entity.Note; +import de.tum.devopss26.noteservice.exception.IllegalNoteAccessException; +import de.tum.devopss26.noteservice.exception.NoteNotFoundException; +import de.tum.devopss26.noteservice.mapper.NoteMapper; +import de.tum.devopss26.noteservice.repository.NoteRepository; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.NonNull; +import org.openapitools.model.*; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; + +import static de.tum.devopss26.noteservice.exception.IllegalNoteAccessException.IllegalAccessPair; + +@Service +@RequiredArgsConstructor +public class NoteServiceImpl implements NoteService { + + private final NoteRepository repository; + private final NoteMapper mapper; + + @Override + public CreateNoteResponse createNote(CreateNoteRequest request, long userId) { + Note note = mapper.toNote(request, userId); + OffsetDateTime now = OffsetDateTime.now(); + note.setCreatedAt(now); + note.setLastUpdatedAt(now); + note = repository.save(note); + return mapper.toCreateResponse(note); + } + + @Override + public ListNotesResponse getNotes(long userId) { + List noteEntities = repository.findAllByUserId(userId); + + List notes = noteEntities + .stream().map(mapper::toIdentifiedTimestamped) + .toList(); + + return mapper.toListResponse(notes); + } + + private @NonNull Note getNoteEntity(long userId, long noteId) { + Optional opt = repository.findById(noteId); + if (opt.isEmpty()) { + throw new NoteNotFoundException(noteId); + } + + Note note = opt.get(); + if (note.getUserId() != userId) { + throw new IllegalNoteAccessException(userId, + new IllegalAccessPair(note.getUserId(), note.getId())); + } + return note; + } + + @Override + public GetNoteResponse getNote(long userId, long id) { + Note note = getNoteEntity(userId, id); + + return mapper.toGetResponse(note); + } + + @Override + public UpdateNoteResponse updateNote(long userId, long id, org.openapitools.model.Note diff) { + Note note = getNoteEntity(userId, id); + + if (diff.getTitle() == null && diff.getContent() == null) { + return mapper.toUpdateResponse(note); + } + + if (diff.getTitle() != null) { + note.setTitle(diff.getTitle()); + } + if (diff.getContent() != null) { + note.setContent(diff.getContent()); + } + + note.setLastUpdatedAt(OffsetDateTime.now()); + note = repository.save(note); + + return mapper.toUpdateResponse(note); + } + + @Override + public void deleteNote(long userId, long id) { + Note note = getNoteEntity(userId, id); + + repository.delete(note); + } +} diff --git a/services/note-service/src/main/resources/db/changelog/db.changelog-v1-create-initial-structure.xml b/services/note-service/src/main/resources/db/changelog/db.changelog-v1-create-initial-structure.xml index ff5d57c..714f1ff 100644 --- a/services/note-service/src/main/resources/db/changelog/db.changelog-v1-create-initial-structure.xml +++ b/services/note-service/src/main/resources/db/changelog/db.changelog-v1-create-initial-structure.xml @@ -4,4 +4,34 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/note-service/src/test/java/de/tum/devopss26/noteservice/controller/NoteControllerTest.java b/services/note-service/src/test/java/de/tum/devopss26/noteservice/controller/NoteControllerTest.java new file mode 100644 index 0000000..3c72523 --- /dev/null +++ b/services/note-service/src/test/java/de/tum/devopss26/noteservice/controller/NoteControllerTest.java @@ -0,0 +1,429 @@ +package de.tum.devopss26.noteservice.controller; + +import de.tum.devopss26.noteservice.exception.IllegalNoteAccessException; +import de.tum.devopss26.noteservice.exception.NoteNotFoundException; +import de.tum.devopss26.noteservice.service.NoteService; +import de.tum.devopss26.shared.exception.GlobalExceptionHandler; +import de.tum.devopss26.shared.security.SecurityAutoConfiguration; +import de.tum.devopss26.shared.security.TokenValidationInterceptor; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openapitools.model.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.OffsetDateTime; +import java.util.List; + +import static com.atlassian.oai.validator.mockmvc.OpenApiValidationMatchers.openApi; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RequiredArgsConstructor +@WebMvcTest(NoteController.class) +@Import({SecurityAutoConfiguration.class, GlobalExceptionHandler.class}) +class NoteControllerTest { + + private MockMvc mockMvc; + + @MockitoBean + private NoteService noteService; + + @MockitoBean + private TokenValidationInterceptor tokenValidationInterceptor; + + private static final String VALID_AUTH_HEADER = "Bearer test-token"; + private static final long USER_ID = 42L; + + @BeforeEach + void setUpInterceptor() throws Exception { + doAnswer(invocation -> { + jakarta.servlet.http.HttpServletRequest request = invocation.getArgument(0); + request.setAttribute("userId", String.valueOf(USER_ID)); + request.setAttribute("jwtClaims", null); + return true; + }).when(tokenValidationInterceptor).preHandle(any(), any(), any()); + } + + @Autowired + public void setMockMvc(MockMvc mockMvc) { + this.mockMvc = mockMvc; + } + + // ───────────────────────────────────────────────────────────────────────── + // POST /api/v1/notes – createNote + // ───────────────────────────────────────────────────────────────────────── + + @Test + void createNote_CREATED() throws Exception { + CreateNoteResponse response = new CreateNoteResponse(); + response.setId(1L); + response.setTitle("Shopping List"); + response.setContent("Buy milk, bread, eggs"); + response.setCreatedAt(OffsetDateTime.parse("2026-07-09T00:00:00Z")); + response.setLastUpdatedAt(OffsetDateTime.parse("2026-07-09T00:00:00Z")); + + when(noteService.createNote(any(CreateNoteRequest.class), eq(USER_ID))) + .thenReturn(response); + + String body = """ + { + "title": "Shopping List", + "content": "Buy milk, bread, eggs" + } + """; + + mockMvc.perform(post("/api/v1/notes") + .header("Authorization", VALID_AUTH_HEADER) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void createNote_UNAUTHORIZED_invalidToken() throws Exception { + doAnswer(invocation -> { + jakarta.servlet.http.HttpServletResponse response = invocation.getArgument(1); + response.sendError(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED, + "Missing or invalid Authorization header"); + return false; + }).when(tokenValidationInterceptor).preHandle(any(), any(), any()); + + String body = """ + { + "title": "Shopping List", + "content": "Buy milk, bread, eggs" + } + """; + + mockMvc.perform(post("/api/v1/notes") + .header("Authorization", "Bearer invalid-token") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isUnauthorized()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void createNote_INTERNAL_SERVER_ERROR() throws Exception { + when(noteService.createNote(any(CreateNoteRequest.class), eq(USER_ID))) + .thenThrow(new RuntimeException("forced error")); + + String body = """ + { + "title": "Shopping List", + "content": "Buy milk, bread, eggs" + } + """; + + mockMvc.perform(post("/api/v1/notes") + .header("Authorization", VALID_AUTH_HEADER) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isInternalServerError()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + // ───────────────────────────────────────────────────────────────────────── + // GET /api/v1/notes – getNotes + // ───────────────────────────────────────────────────────────────────────── + + @Test + void getNotes_OK() throws Exception { + IdentifiedTimestampedNote note = new IdentifiedTimestampedNote(); + note.setId(1L); + note.setTitle("Shopping List"); + note.setContent("Buy milk, bread, eggs"); + note.setCreatedAt(OffsetDateTime.parse("2026-07-09T00:00:00Z")); + note.setLastUpdatedAt(OffsetDateTime.parse("2026-07-09T00:00:00Z")); + + ListNotesResponse response = new ListNotesResponse(); + response.setNotes(List.of(note)); + + when(noteService.getNotes(USER_ID)).thenReturn(response); + + mockMvc.perform(get("/api/v1/notes") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isOk()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void getNotes_UNAUTHORIZED_invalidToken() throws Exception { + doAnswer(invocation -> { + jakarta.servlet.http.HttpServletResponse response = invocation.getArgument(1); + response.sendError(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED, + "Missing or invalid Authorization header"); + return false; + }).when(tokenValidationInterceptor).preHandle(any(), any(), any()); + + mockMvc.perform(get("/api/v1/notes") + .header("Authorization", "Bearer invalid-token")) + .andExpect(status().isUnauthorized()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void getNotes_INTERNAL_SERVER_ERROR() throws Exception { + when(noteService.getNotes(USER_ID)) + .thenThrow(new RuntimeException("forced error")); + + mockMvc.perform(get("/api/v1/notes") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isInternalServerError()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + // ───────────────────────────────────────────────────────────────────────── + // GET /api/v1/notes/{id} – getNoteById + // ───────────────────────────────────────────────────────────────────────── + + @Test + void getNoteById_OK() throws Exception { + GetNoteResponse response = new GetNoteResponse(); + response.setId(1L); + response.setTitle("Shopping List"); + response.setContent("Buy milk, bread, eggs"); + response.setCreatedAt(OffsetDateTime.parse("2026-07-09T00:00:00Z")); + response.setLastUpdatedAt(OffsetDateTime.parse("2026-07-09T00:00:00Z")); + + when(noteService.getNote(USER_ID, 1L)).thenReturn(response); + + mockMvc.perform(get("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isOk()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void getNoteById_NOT_FOUND() throws Exception { + when(noteService.getNote(USER_ID, 99L)) + .thenThrow(new NoteNotFoundException(99L)); + + mockMvc.perform(get("/api/v1/notes/99") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isNotFound()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void getNoteById_FORBIDDEN() throws Exception { + when(noteService.getNote(USER_ID, 1L)) + .thenThrow(new IllegalNoteAccessException(USER_ID, + new IllegalNoteAccessException.IllegalAccessPair(99L, 1L))); + + mockMvc.perform(get("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isForbidden()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void getNoteById_UNAUTHORIZED_invalidToken() throws Exception { + doAnswer(invocation -> { + jakarta.servlet.http.HttpServletResponse response = invocation.getArgument(1); + response.sendError(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED, + "Missing or invalid Authorization header"); + return false; + }).when(tokenValidationInterceptor).preHandle(any(), any(), any()); + + mockMvc.perform(get("/api/v1/notes/1") + .header("Authorization", "Bearer invalid-token")) + .andExpect(status().isUnauthorized()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void getNoteById_INTERNAL_SERVER_ERROR() throws Exception { + when(noteService.getNote(USER_ID, 1L)) + .thenThrow(new RuntimeException("forced error")); + + mockMvc.perform(get("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isInternalServerError()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + // ───────────────────────────────────────────────────────────────────────── + // PUT /api/v1/notes/{id} – updateNote + // ───────────────────────────────────────────────────────────────────────── + + @Test + void updateNote_OK() throws Exception { + UpdateNoteResponse response = new UpdateNoteResponse(); + response.setId(1L); + response.setTitle("Updated Shopping List"); + response.setContent("Buy milk, bread, eggs"); + response.setCreatedAt(OffsetDateTime.parse("2026-07-09T00:00:00Z")); + response.setLastUpdatedAt(OffsetDateTime.parse("2026-07-09T01:00:00Z")); + + when(noteService.updateNote(eq(USER_ID), eq(1L), any(org.openapitools.model.Note.class))) + .thenReturn(response); + + String body = """ + { + "title": "Updated Shopping List" + } + """; + + mockMvc.perform(put("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void updateNote_NOT_FOUND() throws Exception { + when(noteService.updateNote(eq(USER_ID), eq(99L), any(org.openapitools.model.Note.class))) + .thenThrow(new NoteNotFoundException(99L)); + + String body = """ + { + "title": "Updated Shopping List" + } + """; + + mockMvc.perform(put("/api/v1/notes/99") + .header("Authorization", VALID_AUTH_HEADER) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isNotFound()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void updateNote_FORBIDDEN() throws Exception { + when(noteService.updateNote(eq(USER_ID), eq(1L), any(org.openapitools.model.Note.class))) + .thenThrow(new IllegalNoteAccessException(USER_ID, + new IllegalNoteAccessException.IllegalAccessPair(99L, 1L))); + + String body = """ + { + "title": "Updated Shopping List" + } + """; + + mockMvc.perform(put("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isForbidden()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void updateNote_UNAUTHORIZED_invalidToken() throws Exception { + doAnswer(invocation -> { + jakarta.servlet.http.HttpServletResponse response = invocation.getArgument(1); + response.sendError(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED, + "Missing or invalid Authorization header"); + return false; + }).when(tokenValidationInterceptor).preHandle(any(), any(), any()); + + String body = """ + { + "title": "Updated Shopping List" + } + """; + + mockMvc.perform(put("/api/v1/notes/1") + .header("Authorization", "Bearer invalid-token") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isUnauthorized()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void updateNote_INTERNAL_SERVER_ERROR() throws Exception { + when(noteService.updateNote(eq(USER_ID), eq(1L), any(org.openapitools.model.Note.class))) + .thenThrow(new RuntimeException("forced error")); + + String body = """ + { + "title": "Updated Shopping List" + } + """; + + mockMvc.perform(put("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isInternalServerError()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + // ───────────────────────────────────────────────────────────────────────── + // DELETE /api/v1/notes/{id} – deleteNote + // ───────────────────────────────────────────────────────────────────────── + + @Test + void deleteNote_NO_CONTENT() throws Exception { + doNothing().when(noteService).deleteNote(USER_ID, 1L); + + mockMvc.perform(delete("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isNoContent()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void deleteNote_NOT_FOUND() throws Exception { + doThrow(new NoteNotFoundException(99L)) + .when(noteService).deleteNote(USER_ID, 99L); + + mockMvc.perform(delete("/api/v1/notes/99") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isNotFound()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void deleteNote_FORBIDDEN() throws Exception { + doThrow(new IllegalNoteAccessException(USER_ID, + new IllegalNoteAccessException.IllegalAccessPair(99L, 1L))) + .when(noteService).deleteNote(USER_ID, 1L); + + mockMvc.perform(delete("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isForbidden()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void deleteNote_UNAUTHORIZED_invalidToken() throws Exception { + doAnswer(invocation -> { + jakarta.servlet.http.HttpServletResponse response = invocation.getArgument(1); + response.sendError(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED, + "Missing or invalid Authorization header"); + return false; + }).when(tokenValidationInterceptor).preHandle(any(), any(), any()); + + mockMvc.perform(delete("/api/v1/notes/1") + .header("Authorization", "Bearer invalid-token")) + .andExpect(status().isUnauthorized()) + .andExpect(openApi().isValid("note-service.yaml")); + } + + @Test + void deleteNote_INTERNAL_SERVER_ERROR() throws Exception { + doThrow(new RuntimeException("forced error")) + .when(noteService).deleteNote(USER_ID, 1L); + + mockMvc.perform(delete("/api/v1/notes/1") + .header("Authorization", VALID_AUTH_HEADER)) + .andExpect(status().isInternalServerError()) + .andExpect(openApi().isValid("note-service.yaml")); + } +} diff --git a/web-client/src/common.ts b/web-client/src/common.ts index 69d05c0..3545892 100644 --- a/web-client/src/common.ts +++ b/web-client/src/common.ts @@ -10,6 +10,16 @@ export interface components { Identifier: { /** Format: int64 */ id: number; + } & { + [key: string]: unknown; + }; + Timestamped: { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + lastUpdatedAt: string; + } & { + [key: string]: unknown; }; }; responses: never; From 2a70f371e3888f7571baf6c280f216094cd23b74 Mon Sep 17 00:00:00 2001 From: AleWu <77410271+TheAleWu@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:49:05 +0200 Subject: [PATCH 2/2] #54: improvements after PR --- api/calendar-service.yaml | 2 ++ api/note-service.yaml | 5 ++--- .../calendarservice/entity/CalendarEvent.java | 4 ++++ .../IllegalCalendarEventAccessException.java | 16 +++++----------- .../mapper/CalendarEventMapper.java | 2 ++ .../service/CalendarEventServiceImpl.java | 6 ++++++ .../tum/devopss26/noteservice/entity/Note.java | 6 +++++- .../exception/IllegalNoteAccessException.java | 16 +++++----------- .../devopss26/noteservice/mapper/NoteMapper.java | 2 ++ .../noteservice/service/NoteServiceImpl.java | 6 ++++++ .../service/UserAuthenticationServiceImpl.java | 3 +++ 11 files changed, 42 insertions(+), 26 deletions(-) diff --git a/api/calendar-service.yaml b/api/calendar-service.yaml index 56d2f65..82eefe5 100644 --- a/api/calendar-service.yaml +++ b/api/calendar-service.yaml @@ -182,6 +182,8 @@ components: properties: title: type: string + pattern: '.*\S.*' + maxLength: 255 description: type: string startTime: diff --git a/api/note-service.yaml b/api/note-service.yaml index 2ae3f1f..a294bf9 100644 --- a/api/note-service.yaml +++ b/api/note-service.yaml @@ -149,6 +149,8 @@ components: properties: title: type: string + pattern: '.*\S.*' + maxLength: 255 content: type: string ListNotesResponse: @@ -170,15 +172,12 @@ components: CreateNoteResponse: allOf: - $ref: '#/components/schemas/IdentifiedTimestampedNote' - - type: object GetNoteResponse: allOf: - $ref: "#/components/schemas/IdentifiedTimestampedNote" - - type: object UpdateNoteResponse: allOf: - $ref: "#/components/schemas/IdentifiedTimestampedNote" - - type: object IdentifiedNote: type: object allOf: diff --git a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/entity/CalendarEvent.java b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/entity/CalendarEvent.java index 4c71738..704f8e8 100644 --- a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/entity/CalendarEvent.java +++ b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/entity/CalendarEvent.java @@ -6,6 +6,8 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -30,6 +32,8 @@ public class CalendarEvent { @Column(name = "user_id", nullable = false) private Long userId; + @NotBlank + @Size(max = 255) @Column(nullable = false) private String title; diff --git a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/exception/IllegalCalendarEventAccessException.java b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/exception/IllegalCalendarEventAccessException.java index 21c95f6..c458a6b 100644 --- a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/exception/IllegalCalendarEventAccessException.java +++ b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/exception/IllegalCalendarEventAccessException.java @@ -1,20 +1,14 @@ package de.tum.devopss26.calendarservice.exception; import de.tum.devopss26.shared.exception.ForbiddenException; -import org.jspecify.annotations.NonNull; public class IllegalCalendarEventAccessException extends ForbiddenException { - public IllegalCalendarEventAccessException(long accessorId, IllegalAccessPair pair) { - super(accessorId + " tried to illegally access calendar event " + pair.eventId + " of owner " + pair.ownerId); - } + public IllegalCalendarEventAccessException(long accessorId, IllegalAccessPair pair) { + super(accessorId + " tried to illegally access calendar event " + pair.eventId + " of owner " + pair.ownerId); + } - public record IllegalAccessPair(long ownerId, long eventId) { - - @Override - public @NonNull String toString() { - return "(" + ownerId + ", " + eventId + ")"; - } - } + public record IllegalAccessPair(long ownerId, long eventId) { + } } diff --git a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/mapper/CalendarEventMapper.java b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/mapper/CalendarEventMapper.java index 01c8748..d7f32c5 100644 --- a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/mapper/CalendarEventMapper.java +++ b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/mapper/CalendarEventMapper.java @@ -2,6 +2,7 @@ import de.tum.devopss26.calendarservice.entity.CalendarEvent; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.openapitools.model.*; import java.util.List; @@ -16,6 +17,7 @@ public interface CalendarEventMapper { // Specific mappers + @Mapping(target = "userId", source = "userId") CalendarEvent toCalendarEvent(CreateCalendarEventRequest request, long userId); CreateCalendarEventResponse toCreateResponse(CalendarEvent event); diff --git a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/service/CalendarEventServiceImpl.java b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/service/CalendarEventServiceImpl.java index 12a1c0d..bba5385 100644 --- a/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/service/CalendarEventServiceImpl.java +++ b/services/calendar-service/src/main/java/de/tum/devopss26/calendarservice/service/CalendarEventServiceImpl.java @@ -14,6 +14,7 @@ import java.util.Optional; import static de.tum.devopss26.calendarservice.exception.IllegalCalendarEventAccessException.IllegalAccessPair; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -22,6 +23,7 @@ class CalendarEventServiceImpl implements CalendarEventService { private final CalendarEventRepository repository; private final CalendarEventMapper mapper; + @Transactional @Override public CreateCalendarEventResponse createEvent(CreateCalendarEventRequest request, long userId) { CalendarEvent event = mapper.toCalendarEvent(request, userId); @@ -29,6 +31,7 @@ public CreateCalendarEventResponse createEvent(CreateCalendarEventRequest reques return mapper.toCreateResponse(event); } + @Transactional(readOnly = true) @Override public ListCalendarEventResponse getEvents(long userId) { List eventEntities = repository.findAllByUserId(userId); @@ -54,6 +57,7 @@ public ListCalendarEventResponse getEvents(long userId) { return event; } + @Transactional(readOnly = true) @Override public GetCalendarEventResponse getEvent(long userId, long eventId) { CalendarEvent event = getEventEntity(userId, eventId); @@ -61,6 +65,7 @@ public GetCalendarEventResponse getEvent(long userId, long eventId) { return mapper.toGetResponse(event); } + @Transactional @Override public UpdateCalendarEventResponse updateEvent(long userId, long eventId, org.openapitools.model.CalendarEvent diff) { @@ -92,6 +97,7 @@ public UpdateCalendarEventResponse updateEvent(long userId, long eventId, return mapper.toUpdateResponse(event); } + @Transactional @Override public void deleteEvent(long userId, long eventId) { CalendarEvent event = getEventEntity(userId, eventId); diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java index 86433c4..4458495 100644 --- a/services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/entity/Note.java @@ -1,6 +1,8 @@ package de.tum.devopss26.noteservice.entity; import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -24,10 +26,12 @@ public class Note { @Column(name = "user_id", nullable = false) private Long userId; + @NotBlank + @Size(max = 255) @Column(nullable = false) private String title; - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "TEXT") private String content; @Column(name = "created_at", nullable = false) diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java index 3f3e51d..ed11774 100644 --- a/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/exception/IllegalNoteAccessException.java @@ -1,20 +1,14 @@ package de.tum.devopss26.noteservice.exception; import de.tum.devopss26.shared.exception.ForbiddenException; -import org.jspecify.annotations.NonNull; public class IllegalNoteAccessException extends ForbiddenException { - public IllegalNoteAccessException(long accessorId, IllegalAccessPair pair) { - super(accessorId + " tried to illegally access note " + pair.noteId + " of owner " + pair.ownerId); - } + public IllegalNoteAccessException(long accessorId, IllegalAccessPair pair) { + super(accessorId + " tried to illegally access note " + pair.noteId + " of owner " + pair.ownerId); + } - public record IllegalAccessPair(long ownerId, long noteId) { - - @Override - public @NonNull String toString() { - return "(" + ownerId + ", " + noteId + ")"; - } - } + public record IllegalAccessPair(long ownerId, long noteId) { + } } diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java index 0c84043..01d6487 100644 --- a/services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/mapper/NoteMapper.java @@ -2,6 +2,7 @@ import de.tum.devopss26.noteservice.entity.Note; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.openapitools.model.*; import java.util.List; @@ -16,6 +17,7 @@ public interface NoteMapper { // Specific mappers + @Mapping(target = "userId", source = "userId") Note toNote(CreateNoteRequest request, long userId); CreateNoteResponse toCreateResponse(Note note); diff --git a/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java b/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java index 160f240..8062e00 100644 --- a/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java +++ b/services/note-service/src/main/java/de/tum/devopss26/noteservice/service/NoteServiceImpl.java @@ -15,6 +15,7 @@ import java.util.Optional; import static de.tum.devopss26.noteservice.exception.IllegalNoteAccessException.IllegalAccessPair; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -23,6 +24,7 @@ public class NoteServiceImpl implements NoteService { private final NoteRepository repository; private final NoteMapper mapper; + @Transactional @Override public CreateNoteResponse createNote(CreateNoteRequest request, long userId) { Note note = mapper.toNote(request, userId); @@ -33,6 +35,7 @@ public CreateNoteResponse createNote(CreateNoteRequest request, long userId) { return mapper.toCreateResponse(note); } + @Transactional(readOnly = true) @Override public ListNotesResponse getNotes(long userId) { List noteEntities = repository.findAllByUserId(userId); @@ -58,6 +61,7 @@ public ListNotesResponse getNotes(long userId) { return note; } + @Transactional(readOnly = true) @Override public GetNoteResponse getNote(long userId, long id) { Note note = getNoteEntity(userId, id); @@ -65,6 +69,7 @@ public GetNoteResponse getNote(long userId, long id) { return mapper.toGetResponse(note); } + @Transactional @Override public UpdateNoteResponse updateNote(long userId, long id, org.openapitools.model.Note diff) { Note note = getNoteEntity(userId, id); @@ -86,6 +91,7 @@ public UpdateNoteResponse updateNote(long userId, long id, org.openapitools.mode return mapper.toUpdateResponse(note); } + @Transactional @Override public void deleteNote(long userId, long id) { Note note = getNoteEntity(userId, id); diff --git a/services/user-service/src/main/java/de/tum/devopss26/userservice/service/UserAuthenticationServiceImpl.java b/services/user-service/src/main/java/de/tum/devopss26/userservice/service/UserAuthenticationServiceImpl.java index 436f8e2..ba39247 100644 --- a/services/user-service/src/main/java/de/tum/devopss26/userservice/service/UserAuthenticationServiceImpl.java +++ b/services/user-service/src/main/java/de/tum/devopss26/userservice/service/UserAuthenticationServiceImpl.java @@ -10,6 +10,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Objects; import java.util.Optional; @@ -23,6 +24,7 @@ class UserAuthenticationServiceImpl implements UserAuthenticationService { private final UserMapper mapper; private final JwtService jwtService; + @Transactional @Override public void registerUser(RegisterUserRequest request) { if (repository.existsByUsername(request.getUsername())) { @@ -33,6 +35,7 @@ public void registerUser(RegisterUserRequest request) { repository.save(mapped); } + @Transactional(readOnly = true) @Override public String loginUser() { String username = Objects.requireNonNull(SecurityContextHolder.getContext().getAuthentication()).getName();