Compare commits

...
13 Commits
Author SHA1 Message Date
tokishu eea18d335b Chore: Update README.md
- my thoughts
2026-09-13 02:30:01 +02:00
tokishu 42b2d42878 Merge pull request #3 from Tok1shu/1.0.0-pre-admin
Just pull request
2026-09-13 02:05:23 +02:00
Tokishu 0aeafc4f56 Pre admin 2025-06-02 22:23:43 +02:00
Tokishu aceeb32d69 Just push 2025-05-31 23:05:43 +02:00
Tokishu ca10835732 Merge remote-tracking branch 'origin/master' 2025-05-31 02:31:30 +02:00
Tokishu e00ac3fd66 с -> c 2025-05-31 02:31:06 +02:00
Tokishu 75d37a160a Update README.md 2025-05-30 17:19:15 -07:00
Tokishu e86b8bf08e Added users logic to the NoteService
Fix password leak by introducing NoteResponse DTO
Fix typo: "serurity" → "security"
2025-05-31 02:10:11 +02:00
Tokishu 078ff4fb41 I accidentally pushed into that branch :(
Added JWT
2025-05-26 17:25:18 -07:00
Tokishu 1e003c7a10 Added JWT 2025-05-27 02:23:52 +02:00
Tokishu 5ff716e515 Merge pull request #1 from Tok1shu/db-add-request
Migrated to DB use
2025-05-23 14:29:52 -07:00
Tokishu 7dae1d56bf Update README.md 2025-05-15 03:32:11 -07:00
Tokishu 4b108e1ae1 Create README.md 2025-05-15 01:42:23 +02:00
44 changed files with 1497 additions and 231 deletions
+14
View File
@@ -0,0 +1,14 @@
# Spring Boot Note Api
> I'm testing spring boot, and it might become my main framework for building APIs
---
486 days have passed since I started this project, well, lets rewrite it to DDD + clean and just finish that mini project.
I created an archive branch for old code.
but for alll that time i didnt learn how to make normal tests for my code, cuz i
learned spring boot and java core superfically
i think it will be better to make tests "in the progress", eeeh, for example: new feature, some tests for that, one more feature...
and so on.
+39
View File
@@ -74,6 +74,45 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Security -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.5.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
</dependencies>
<build>
@@ -0,0 +1,9 @@
package net.tokishu.note.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
}
@@ -0,0 +1,9 @@
package net.tokishu.note.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NullableCurrentUser {
}
@@ -0,0 +1,65 @@
package net.tokishu.note.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.security.JwtService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,46 @@
package net.tokishu.note.config;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.exception.SecurityExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final SecurityExceptionHandler securityExceptionHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(securityExceptionHandler)
.accessDeniedHandler(securityExceptionHandler)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/auth/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/notes/*")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/error")).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@@ -0,0 +1,24 @@
package net.tokishu.note.config;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.resolver.CurrentUserArgumentResolver;
import net.tokishu.note.resolver.NullableCurrentUserArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final CurrentUserArgumentResolver currentUserArgumentResolver;
private final NullableCurrentUserArgumentResolver nullableCurrentUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
resolvers.add(nullableCurrentUserArgumentResolver);
}
}
@@ -0,0 +1,47 @@
package net.tokishu.note.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.request.LoginRequest;
import net.tokishu.note.dto.request.RegisterRequest;
import net.tokishu.note.dto.response.LoginResponse;
import net.tokishu.note.dto.response.RegisterResponse;
import net.tokishu.note.dto.response.RootResponse;
import net.tokishu.note.model.User;
import net.tokishu.note.service.AuthService;
import net.tokishu.note.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequiredArgsConstructor
@Validated
@RequestMapping("/auth")
public class AuthController {
public final AuthService authService;
@PostMapping("/register")
public ResponseEntity<RegisterResponse> register(@RequestBody @Valid RegisterRequest request) {
RegisterResponse response = authService.registerUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
LoginResponse response = authService.authorizeUser(request);
return ResponseEntity.ok(response);
}
@PostMapping("/new-root")
public ResponseEntity<RootResponse> newRoot() {
RootResponse response = authService.registerRoot();
return ResponseEntity.ok(response);
}
}
@@ -1,49 +1,55 @@
package net.tokishu.note.controller;
import net.tokishu.note.dto.NoteRequest;
import net.tokishu.note.model.Note;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.annotation.CurrentUser;
import net.tokishu.note.annotation.NullableCurrentUser;
import net.tokishu.note.dto.request.NoteRequest;
import net.tokishu.note.dto.response.NoteResponse;
import net.tokishu.note.model.User;
import net.tokishu.note.service.NoteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@RequiredArgsConstructor
@RestController
@Validated
@RequestMapping("/notes")
public class NotesController {
@Autowired
private NoteService noteService;
private final NoteService noteService;
@GetMapping("/notes")
public ResponseEntity<?> getAll(){
return ResponseEntity.ok(noteService.getAll());
@GetMapping
public ResponseEntity<List<NoteResponse>> getAll(@CurrentUser User sender){
return ResponseEntity.ok(noteService.getAll(sender));
}
@GetMapping("/note/{uuid}")
public Note find(@PathVariable UUID uuid){
return noteService.find(uuid);
@GetMapping("/{idOrCode}")
public NoteResponse find(@PathVariable String idOrCode, @NullableCurrentUser User sender) {
return noteService.findByIdOrPublicLink(idOrCode, sender);
}
@PostMapping("/note")
public ResponseEntity<?> add(@RequestBody NoteRequest note){
noteService.add(note);
@PostMapping()
public ResponseEntity<Map<String, String>> add(@RequestBody @Valid NoteRequest note, @CurrentUser User sender){
noteService.add(note, sender);
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "Note added"));
}
@PutMapping("/note/{uuid}")
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody NoteRequest note){
noteService.update(uuid, note);
@PutMapping("/{uuid}")
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody @Valid NoteRequest note, @CurrentUser User sender){
noteService.update(uuid, note, sender);
return ResponseEntity.ok(Map.of("message", "Note updated"));
}
@DeleteMapping("/note/{uuid}")
public ResponseEntity<?> delete(@PathVariable UUID uuid){
noteService.delete(uuid);
@DeleteMapping("/{uuid}")
public ResponseEntity<?> delete(@PathVariable UUID uuid, @CurrentUser User sender){
noteService.delete(uuid, sender);
return ResponseEntity.ok(Map.of("message", "Note deleted"));
}
}
@@ -0,0 +1,79 @@
package net.tokishu.note.controller;
import jakarta.validation.Valid;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.annotation.CurrentUser;
import net.tokishu.note.dto.request.ChangePasswordRequest;
import net.tokishu.note.dto.request.UpdateProfileRequest;
import net.tokishu.note.dto.request.UserByAdminRequest;
import net.tokishu.note.dto.response.ApiResponse;
import net.tokishu.note.dto.response.UserResponse;
import net.tokishu.note.model.User;
import net.tokishu.note.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@GetMapping("/me")
public UserResponse me(@CurrentUser User sender){
return userService.getCurrentUserResponse(sender);
}
@PutMapping("/me")
public UserResponse updateMyProfile(@RequestBody @Valid UpdateProfileRequest request, @CurrentUser User sender) {
return userService.updateCurrentUser(request, sender);
}
@PutMapping("/me/password")
public ApiResponse changePassword(@RequestBody @Valid ChangePasswordRequest request, @CurrentUser User sender) {
// userService.changePassword(request);
return ApiResponse.builder().status(200).message("JoJ").build();
}
/*
ADMIN ACTIONS
*/
// @PostMapping("/new")
// @PreAuthorize("hasAuthority('ADMIN')")
// public ResponseEntity<?> newUser(@RequestBody @Valid UserByAdminRequest request, @CurrentUser User sender){
// return userService.createUserByAdmin(request);
// }
//
// @GetMapping
// @PreAuthorize("hasAuthority('ADMIN')")
// public ResponseEntity<List<UserResponse>> getAllUsers(@CurrentUser User sender) {
// return userService.getAllUsers();
// }
//
// @GetMapping("/{username}")
// @PreAuthorize("hasAuthority('ADMIN')")
// public UserResponse getUserByUsername(@PathVariable String username, @CurrentUser User sender) {
// return userService.getUserByUsername(username);
// }
//
//
// @PutMapping("/{username}")
// @PreAuthorize("hasAuthority('ADMIN')")
// public UserResponse editUser(@RequestBody @Valid UserByAdminRequest request, @PathVariable String username, @CurrentUser User sender){
// return userService.editUser(username, request);
// }
//
// @DeleteMapping("/{username}")
// @PreAuthorize("hasAuthority('ADMIN')")
// public ApiResponse removeUser(@PathVariable String username, @CurrentUser User sender){
// return userService.deleteUser(username);
// }
}
@@ -1,9 +0,0 @@
package net.tokishu.note.dto;
import lombok.Data;
@Data
public class NoteRequest {
private String name;
private String text;
}
@@ -0,0 +1,13 @@
package net.tokishu.note.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ChangePasswordRequest {
@NotBlank
private String oldPassword;
@NotBlank
private String newPassword;
}
@@ -0,0 +1,13 @@
package net.tokishu.note.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
}
@@ -0,0 +1,20 @@
package net.tokishu.note.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class NoteRequest {
@NotBlank
@Size(min=1, max=128)
private String name;
@NotBlank
@Size(min=1, max=8192)
private String text;
@NotNull
private Boolean isPublic;
}
@@ -0,0 +1,19 @@
package net.tokishu.note.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class RegisterRequest {
@NotBlank()
@Size(min = 3, max = 24)
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
message = "The username can only contain letters, numbers, symbols and hyphens.")
private String username;
@NotBlank()
@Size(min = 6, max = 42)
private String password;
}
@@ -0,0 +1,18 @@
package net.tokishu.note.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class UpdateProfileRequest {
@NotBlank()
@Size(min = 3, max = 24)
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
message = "The username can only contain letters, numbers, symbols and hyphens.")
private String username;
@NotBlank()
private String gravatarEmail;
}
@@ -0,0 +1,23 @@
package net.tokishu.note.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import net.tokishu.note.model.UserRole;
@Data
public class UserByAdminRequest {
@NotBlank()
@Size(min = 3, max = 24)
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
message = "The username can only contain letters, numbers, symbols and hyphens.")
private String username;
@NotBlank()
@Size(min = 6, max = 42)
private String password;
@NotBlank
private UserRole role;
}
@@ -0,0 +1,5 @@
package net.tokishu.note.dto.request;
public class UserRequest {
}
@@ -0,0 +1,11 @@
package net.tokishu.note.dto.response;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class ApiResponse {
private final int status;
private final String message;
}
@@ -0,0 +1,13 @@
package net.tokishu.note.dto.response;
import lombok.Builder;
import lombok.Data;
import net.tokishu.note.model.UserRole;
@Data
@Builder
public class LoginResponse {
private String token;
private String username;
private UserRole role;
}
@@ -0,0 +1,19 @@
package net.tokishu.note.dto.response;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@Builder
public class NoteResponse {
private UUID uuid;
private String name;
private String text;
private Boolean isPublic;
private String publicLink;
private String author;
private LocalDateTime createdAt;
}
@@ -0,0 +1,14 @@
package net.tokishu.note.dto.response;
import lombok.Builder;
import lombok.Data;
import net.tokishu.note.model.UserRole;
@Data
@Builder
public class RegisterResponse {
private String username;
private UserRole role;
private String message;
}
@@ -0,0 +1,13 @@
package net.tokishu.note.dto.response;
import lombok.Builder;
import lombok.Data;
import net.tokishu.note.model.UserRole;
@Data
@Builder
public class RootResponse {
private String username;
private String password;
private UserRole role;
}
@@ -0,0 +1,13 @@
package net.tokishu.note.dto.response;
import lombok.Data;
import lombok.Builder;
import net.tokishu.note.model.UserRole;
@Data
@Builder
public class UserResponse {
private String username;
private String gravatarUrl;
private UserRole role;
}
@@ -0,0 +1,67 @@
package net.tokishu.note.exception;
import net.tokishu.note.dto.response.ApiResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ResponseStatusException;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handleAll(Exception ex) {
if (ex instanceof MethodArgumentNotValidException validationEx) {
return handleValidation(validationEx);
}
switch (ex.getClass().getSimpleName()) {
case "ResponseStatusException" -> {
ResponseStatusException rse = (ResponseStatusException) ex;
return buildErrorResponse(rse.getStatusCode().value(), rse.getReason());
}
case "NoHandlerFoundException" -> {
return buildErrorResponse(404, "Endpoint not found");
}
case "HttpMessageNotReadableException" -> {
return buildErrorResponse(400, "Invalid request body");
}
case "HttpRequestMethodNotSupportedException" -> {
return buildErrorResponse(405, "Method not allowed");
}
case "HttpMediaTypeNotSupportedException" -> {
return buildErrorResponse(415, "Unsupported media type");
}
case "HttpMediaTypeNotAcceptableException" -> {
return buildErrorResponse(406, "Not acceptable");
}
case "HttpRequestTimeoutException" -> {
return buildErrorResponse(408, "Request timeout");
}
default -> {
return buildErrorResponse(500, "Internal server error");
}
}
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.findFirst()
.orElse("Invalid request");
return buildErrorResponse(400, message);
}
private ResponseEntity<ApiResponse> buildErrorResponse(int status, String message) {
return ResponseEntity
.status(status)
.body(ApiResponse.builder()
.status(status)
.message(message != null ? message : "Unexpected error")
.build());
}
}
@@ -0,0 +1,41 @@
package net.tokishu.note.exception;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.tokishu.note.dto.response.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class SecurityExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
private void writeErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ApiResponse body = ApiResponse.builder()
.status(status)
.message(message)
.build();
response.getWriter().write(objectMapper.writeValueAsString(body));
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
org.springframework.security.core.AuthenticationException authException) throws IOException {
writeErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
writeErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}
}
@@ -1,9 +1,14 @@
package net.tokishu.note.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Null;
import lombok.Builder;
import lombok.Data;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@@ -13,6 +18,22 @@ public class Note {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID uuid;
@Column(length = 128, nullable = false)
private String name;
@Column(length = 8192, nullable = false)
private String text;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author", referencedColumnName = "username")
private User author;
@Column(length = 6, nullable = true, updatable = false)
private String publicLink;
@ColumnDefault("false")
private Boolean isPublic;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
}
@@ -0,0 +1,34 @@
package net.tokishu.note.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import lombok.Data;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Data
@Table(name = "Users")
public class User {
@Id
@Column(length = 24)
private String username;
@Column(nullable = false)
private String password;
@Enumerated(EnumType.STRING)
@ColumnDefault("'USER'")
private UserRole role;
@Email
@Column(nullable = true)
private String gravatarEmail;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
}
@@ -0,0 +1,5 @@
package net.tokishu.note.model;
public enum UserRole {
ADMIN, USER
}
@@ -1,8 +1,22 @@
package net.tokishu.note.repo;
import net.tokishu.note.model.Note;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface NoteRepository extends JpaRepository<net.tokishu.note.model.Note, UUID> {
public interface NoteRepository extends JpaRepository<Note, UUID> {
List<Note> findByAuthorUsername(String username);
List<Note> findByAuthorUsernameOrderByCreatedAtDesc(String username);
Optional<Note> findByPublicLinkAndIsPublicTrue(String publicLink);
boolean existsByPublicLink(String publicLink);
@Modifying
@Query("UPDATE Note n SET n.author = :newUsername WHERE n.author = :oldUsername")
void updateOwnerUsername(@Param("oldUsername") String oldUsername, @Param("newUsername") String newUsername);
}
@@ -0,0 +1,19 @@
package net.tokishu.note.repo;
import net.tokishu.note.model.User;
import net.tokishu.note.model.UserRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.UUID;
public interface UserRepository extends JpaRepository<User, String> {
boolean existsByRole(UserRole role);
@Modifying
@Query("UPDATE User u SET u.username = :newUsername WHERE u.username = :oldUsername")
void updateUsername(@Param("oldUsername") String oldUsername, @Param("newUsername") String newUsername);
}
@@ -0,0 +1,33 @@
package net.tokishu.note.resolver;
import net.tokishu.note.annotation.CurrentUser;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.model.User;
import net.tokishu.note.service.AuthService;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
@RequiredArgsConstructor
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthService authService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class)
&& parameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
return authService.getCurrentUser();
}
}
@@ -0,0 +1,33 @@
package net.tokishu.note.resolver;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.annotation.NullableCurrentUser;
import net.tokishu.note.model.User;
import net.tokishu.note.service.AuthService;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
@RequiredArgsConstructor
public class NullableCurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthService authService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(NullableCurrentUser.class)
&& parameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
return authService.getCurrentUserOrNull();
}
}
@@ -0,0 +1,28 @@
package net.tokishu.note.security;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.model.User;
import net.tokishu.note.repo.UserRepository;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findById(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole()))
);
}
}
@@ -0,0 +1,73 @@
package net.tokishu.note.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.security.Key;
import java.util.Date;
import java.util.function.Function;
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24 hours
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// JwtService.java
public Claims extractAllClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token expired");
}
}
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
@@ -0,0 +1,119 @@
package net.tokishu.note.service;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.request.LoginRequest;
import net.tokishu.note.dto.request.RegisterRequest;
import net.tokishu.note.dto.response.LoginResponse;
import net.tokishu.note.dto.response.RegisterResponse;
import net.tokishu.note.dto.response.RootResponse;
import net.tokishu.note.dto.response.UserResponse;
import net.tokishu.note.model.User;
import net.tokishu.note.model.UserRole;
import net.tokishu.note.repo.UserRepository;
import net.tokishu.note.security.JwtService;
import net.tokishu.note.util.GravatarUtil;
import net.tokishu.note.util.PasswordGenerator;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final UserService userService;
public RootResponse registerRoot() {
String rawPassword = PasswordGenerator.generate(16);
User rootUser = userService.createUser("root", rawPassword, UserRole.ADMIN, null);
return RootResponse.builder()
.username(rootUser.getUsername())
.password(rawPassword)
.role(rootUser.getRole())
.build();
}
public RegisterResponse registerUser(RegisterRequest request) {
User user = userService.createRegularUser(request.getUsername(), request.getPassword());
return RegisterResponse.builder()
.username(user.getUsername())
.role(user.getRole())
.message("Registration successful")
.build();
}
public LoginResponse authorizeUser(LoginRequest request) {
User user = userRepository.findById(request.getUsername())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"));
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
}
String token = jwtService.generateToken(
org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole().name())
.build()
);
return LoginResponse.builder()
.token(token)
.username(user.getUsername())
.role(user.getRole())
.build();
}
public User getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated");
}
Object principal = authentication.getPrincipal();
String username;
if (principal instanceof UserDetails userDetails) {
username = userDetails.getUsername();
} else if (principal instanceof String) {
username = (String) principal;
} else {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unable to identify user");
}
return userRepository.findById(username)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found"));
}
public User getCurrentUserOrNull() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
String username;
if (principal instanceof UserDetails userDetails) {
username = userDetails.getUsername();
} else if (principal instanceof String) {
username = (String) principal;
} else {
return null;
}
return userRepository.findById(username).orElse(null);
}
}
@@ -1,52 +1,137 @@
package net.tokishu.note.service;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.NoteRequest;
import net.tokishu.note.dto.request.NoteRequest;
import net.tokishu.note.dto.response.NoteResponse;
import net.tokishu.note.model.Note;
import net.tokishu.note.model.User;
import net.tokishu.note.model.UserRole;
import net.tokishu.note.repo.NoteRepository;
import net.tokishu.note.util.CheckAuthUtil;
import net.tokishu.note.util.CodeGenerator;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class NoteService {
public final NoteRepository noteRepository;
public final UserService userService;
public List<Note> getAll(){
return noteRepository.findAll();
public List<NoteResponse> getAll(User currentUser) {
List<Note> notes;
if (currentUser.getRole() == UserRole.ADMIN) {
notes = noteRepository.findAll();
} else {
notes = noteRepository.findByAuthorUsername(currentUser.getUsername());
}
public Note find(UUID uuid) {
return noteRepository.findById(uuid)
return notes.stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
public NoteResponse findByIdOrPublicLink(String idOrCode, User currentUser) {
if (!StringUtils.hasText(idOrCode)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid identifier or public link");
}
Note note;
if (isUuid(idOrCode)) {
CheckAuthUtil.check(currentUser);
UUID id = UUID.fromString(idOrCode);
note = noteRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
checkOwnership(note, currentUser);
} else {
note = noteRepository.findByPublicLinkAndIsPublicTrue(idOrCode)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
}
return toResponse(note);
}
public Note add(NoteRequest data){
public NoteResponse add(NoteRequest data, User author) {
Note note = new Note();
note.setName(data.getName());
note.setText(data.getText());
note.setAuthor(author);
return noteRepository.save(note);
String code;
int attempts = 0;
do {
code = CodeGenerator.generateCode();
attempts++;
if (attempts > 10) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate unique public link");
}
} while (noteRepository.existsByPublicLink(code));
note.setPublicLink(code);
note.setIsPublic(Boolean.TRUE.equals(data.getIsPublic()));
Note saved = noteRepository.save(note);
return toResponse(saved);
}
public Note update(UUID uuid, NoteRequest data){
public NoteResponse update(UUID uuid, NoteRequest data, User currentUser) {
Note existing = noteRepository.findById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
checkOwnership(existing, currentUser);
existing.setName(data.getName());
existing.setText(data.getText());
existing.setIsPublic(Boolean.TRUE.equals(data.getIsPublic()));
return noteRepository.save(existing);
return toResponse(noteRepository.save(existing));
}
public void delete(UUID uuid){
public void delete(UUID uuid, User currentUser) {
Note existing = noteRepository.findById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
noteRepository.deleteById(uuid);
checkOwnership(existing, currentUser);
noteRepository.delete(existing);
}
private void checkOwnership(Note note, User user) {
if (user.getRole() == UserRole.ADMIN) {
return;
}
if (!note.getAuthor().getUsername().equals(user.getUsername())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not the author of this note");
}
}
private NoteResponse toResponse(Note note) {
return NoteResponse.builder()
.uuid(note.getUuid())
.name(note.getName())
.text(note.getText())
.isPublic(note.getIsPublic())
.publicLink(note.getPublicLink())
.author(note.getAuthor().getUsername())
.createdAt(note.getCreatedAt())
.build();
}
private boolean isUuid(String input) {
try {
UUID.fromString(input);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
@@ -0,0 +1,88 @@
package net.tokishu.note.service;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.request.UpdateProfileRequest;
import net.tokishu.note.dto.response.UserResponse;
import net.tokishu.note.model.User;
import net.tokishu.note.model.UserRole;
import net.tokishu.note.repo.NoteRepository;
import net.tokishu.note.repo.UserRepository;
import net.tokishu.note.util.CheckAuthUtil;
import net.tokishu.note.util.GravatarUtil;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final NoteRepository noteRepository;
public UserResponse getCurrentUserResponse(User user) {
return UserResponse.builder()
.username(user.getUsername())
.role(user.getRole())
.gravatarUrl(GravatarUtil.generateGravatarUrl(user.getGravatarEmail()))
.build();
}
public User createUser(String username, String password, UserRole role, User currentUser) {
if (userRepository.existsById(username)) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "User already exists");
}
boolean adminExists = userRepository.existsByRole(UserRole.ADMIN);
boolean isCreatingAdmin = UserRole.ADMIN.equals(role);
if (isCreatingAdmin && adminExists) {
if (currentUser == null || currentUser.getRole() != UserRole.ADMIN) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Only admin can create another admin");
}
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setRole(role);
return userRepository.save(user);
}
public User createRegularUser(String username, String password) {
return createUser(username, password, UserRole.USER, null);
}
@Transactional
public UserResponse updateCurrentUser(UpdateProfileRequest request, User currentUser) {
String oldUsername = currentUser.getUsername();
String newUsername = request.getUsername();
if (!oldUsername.equals(newUsername)) {
if (userRepository.existsById(newUsername)) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Username is already taken");
}
userRepository.updateUsername(oldUsername, newUsername);
noteRepository.updateOwnerUsername(oldUsername, newUsername);
currentUser.setUsername(newUsername);
}
currentUser.setGravatarEmail(request.getGravatarEmail());
User saved = userRepository.save(currentUser);
return UserResponse.builder()
.username(saved.getUsername())
.role(currentUser.getRole())
.gravatarUrl(GravatarUtil.generateGravatarUrl(saved.getGravatarEmail()))
.build();
}
}
@@ -0,0 +1,15 @@
package net.tokishu.note.util;
import lombok.experimental.UtilityClass;
import net.tokishu.note.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
@UtilityClass
public class CheckAuthUtil {
public void check(User user) {
if (user == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated");
}
}
}
@@ -0,0 +1,21 @@
package net.tokishu.note.util;
import lombok.experimental.UtilityClass;
import java.security.SecureRandom;
import java.util.Random;
@UtilityClass
public class CodeGenerator {
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final int LENGTH = 6;
private static final Random RANDOM = new SecureRandom();
public String generateCode() {
StringBuilder sb = new StringBuilder(LENGTH);
for (int i = 0; i < LENGTH; i++) {
sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
}
return sb.toString();
}
}
@@ -0,0 +1,28 @@
package net.tokishu.note.util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class GravatarUtil {
public static String generateGravatarUrl(String email) {
if (email == null || email.isBlank()) {
return null;
}
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(email.trim().toLowerCase().getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return "https://www.gravatar.com/avatar/" + sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not supported", e);
}
}
}
@@ -0,0 +1,19 @@
package net.tokishu.note.util;
import lombok.experimental.UtilityClass;
import java.security.SecureRandom;
@UtilityClass
public class PasswordGenerator {
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*!$%&@";
private static final SecureRandom RANDOM = new SecureRandom();
public String generate(int length) {
StringBuilder password = new StringBuilder();
for (int i = 0; i < length; i++) {
password.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
}
return password.toString();
}
}
@@ -1,6 +1,10 @@
spring.application.name=notes
spring.mvc.servlet.path=/api
# Error
spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=false
# DB
spring.datasource.url=jdbc:postgresql://localhost:5432/notes
spring.datasource.username=postgres
@@ -9,3 +13,5 @@ spring.jpa.hibernate.ddl-auto=update
# Disable trace
server.error.include-stacktrace=never
jwt.secret=f037435dc264ba72492096d3f3eb2f3da046f75ce45c85bc9bb487caa2bedb60
@@ -1,6 +1,7 @@
package net.tokishu.note.service;
import net.tokishu.note.dto.NoteRequest;
import net.tokishu.note.dto.request.NoteRequest;
import net.tokishu.note.dto.response.NoteResponse;
import net.tokishu.note.model.Note;
import net.tokishu.note.repo.NoteRepository;
import org.junit.jupiter.api.BeforeEach;
@@ -20,183 +21,176 @@ import static org.junit.jupiter.api.Assertions.*;
@ActiveProfiles("test") // Используем тестовый профиль
@Transactional // Откатывает транзакции после каждого теста
public class NoteServiceTest {
@Autowired
private NoteService noteService;
@Autowired
private NoteRepository noteRepository;
@BeforeEach
void setUp() {
// Очищаем БД перед каждым тестом
noteRepository.deleteAll();
}
@Test
void addAndGetAllNotes() {
// Given
NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Test Note");
noteRequest.setText("Test content");
// When
Note savedNote = noteService.add(noteRequest);
List<Note> notes = noteService.getAll();
// Then
assertNotNull(savedNote);
assertNotNull(savedNote.getUuid());
assertEquals(1, notes.size());
assertEquals("Test Note", notes.get(0).getName());
assertEquals("Test content", notes.get(0).getText());
}
@Test
void findExistingNote() {
// Given
NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Find Me");
noteRequest.setText("Find content");
Note savedNote = noteService.add(noteRequest);
// When
Note foundNote = noteService.find(savedNote.getUuid());
// Then
assertNotNull(foundNote);
assertEquals("Find Me", foundNote.getName());
assertEquals("Find content", foundNote.getText());
assertEquals(savedNote.getUuid(), foundNote.getUuid());
}
@Test
void findNonExistingNote_ShouldThrowException() {
// Given
UUID nonExistingId = UUID.randomUUID();
// When & Then
ResponseStatusException exception = assertThrows(
ResponseStatusException.class,
() -> noteService.find(nonExistingId)
);
assertTrue(exception.getMessage().contains("Note not found"));
}
@Test
void updateExistingNote() {
// Given
NoteRequest originalRequest = new NoteRequest();
originalRequest.setName("Original Name");
originalRequest.setText("Original content");
Note savedNote = noteService.add(originalRequest);
NoteRequest updateRequest = new NoteRequest();
updateRequest.setName("Updated Name");
updateRequest.setText("Updated content");
// When
Note updatedNote = noteService.update(savedNote.getUuid(), updateRequest);
// Then
assertNotNull(updatedNote);
assertEquals(savedNote.getUuid(), updatedNote.getUuid());
assertEquals("Updated Name", updatedNote.getName());
assertEquals("Updated content", updatedNote.getText());
// Проверяем, что изменения сохранились в БД
Note foundNote = noteService.find(savedNote.getUuid());
assertEquals("Updated Name", foundNote.getName());
assertEquals("Updated content", foundNote.getText());
}
@Test
void updateNonExistingNote_ShouldThrowException() {
// Given
UUID nonExistingId = UUID.randomUUID();
NoteRequest updateRequest = new NoteRequest();
updateRequest.setName("Update Name");
updateRequest.setText("Update content");
// When & Then
ResponseStatusException exception = assertThrows(
ResponseStatusException.class,
() -> noteService.update(nonExistingId, updateRequest)
);
assertTrue(exception.getMessage().contains("Note not found"));
}
@Test
void deleteExistingNote() {
// Given
NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Delete Me");
noteRequest.setText("Delete content");
Note savedNote = noteService.add(noteRequest);
// When
assertDoesNotThrow(() -> noteService.delete(savedNote.getUuid()));
// Then
List<Note> notes = noteService.getAll();
assertTrue(notes.isEmpty());
// Проверяем, что заметка действительно удалена
ResponseStatusException exception = assertThrows(
ResponseStatusException.class,
() -> noteService.find(savedNote.getUuid())
);
assertTrue(exception.getMessage().contains("Note not found"));
}
@Test
void deleteNonExistingNote_ShouldThrowException() {
// Given
UUID nonExistingId = UUID.randomUUID();
// When & Then
ResponseStatusException exception = assertThrows(
ResponseStatusException.class,
() -> noteService.delete(nonExistingId)
);
assertTrue(exception.getMessage().contains("Note not found"));
}
@Test
void getAllNotes_WhenEmpty() {
// When
List<Note> notes = noteService.getAll();
// Then
assertNotNull(notes);
assertTrue(notes.isEmpty());
}
@Test
void getAllNotes_WithMultipleNotes() {
// Given
for (int i = 1; i <= 5; i++) {
NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Note " + i);
noteRequest.setText("Content for note " + i);
noteService.add(noteRequest);
}
// When
List<Note> notes = noteService.getAll();
// Then
assertNotNull(notes);
assertEquals(5, notes.size());
// Проверяем, что все заметки уникальны по ID
long uniqueIds = notes.stream()
.map(Note::getUuid)
.distinct()
.count();
assertEquals(5, uniqueIds);
}
//
// @Autowired
// private NoteService noteService;
//
// @Autowired
// private NoteRepository noteRepository;
//
// @BeforeEach
// void setUp() {
// // Очищаем БД перед каждым тестом
// noteRepository.deleteAll();
// }
//
// @Test
// void addAndGetAllNotes() {
// // Given
// NoteRequest noteRequest = new NoteRequest();
// noteRequest.setName("Test Note");
// noteRequest.setText("Test content");
//
// // When
// NoteResponse savedNote = noteService.add(noteRequest);
// List<NoteResponse> notes = noteService.getAll();
//
// // Then
// assertNotNull(savedNote);
// assertNotNull(savedNote.getUuid());
// assertEquals(1, notes.size());
// assertEquals("Test Note", notes.get(0).getName());
// assertEquals("Test content", notes.get(0).getText());
// }
//
// @Test
// void findExistingNote() {
// // Given
// NoteRequest noteRequest = new NoteRequest();
// noteRequest.setName("Find Me");
// noteRequest.setText("Find content");
// NoteResponse savedNote = noteService.add(noteRequest);
//
// // When
// NoteResponse foundNote = noteService.find(savedNote.getUuid());
//
// // Then
// assertNotNull(foundNote);
// assertEquals("Find Me", foundNote.getName());
// assertEquals("Find content", foundNote.getText());
// assertEquals(savedNote.getUuid(), foundNote.getUuid());
// }
//
// @Test
// void findNonExistingNote_ShouldThrowException() {
// // Given
// UUID nonExistingId = UUID.randomUUID();
//
// // When & Then
// ResponseStatusException exception = assertThrows(
// ResponseStatusException.class,
// () -> noteService.find(nonExistingId)
// );
//
// assertTrue(exception.getMessage().contains("Note not found"));
// }
//
// @Test
// void updateExistingNote() {
// // Given
// NoteRequest originalRequest = new NoteRequest();
// originalRequest.setName("Original Name");
// originalRequest.setText("Original content");
// NoteResponse savedNote = noteService.add(originalRequest);
//
// NoteRequest updateRequest = new NoteRequest();
// updateRequest.setName("Updated Name");
// updateRequest.setText("Updated content");
//
// // When
// NoteResponse updatedNote = noteService.update(savedNote.getUuid(), updateRequest);
//
// // Then
// assertNotNull(updatedNote);
// assertEquals(savedNote.getUuid(), updatedNote.getUuid());
// assertEquals("Updated Name", updatedNote.getName());
// assertEquals("Updated content", updatedNote.getText());
//
// // Проверяем, что изменения сохранились в БД
// NoteResponse foundNote = noteService.find(savedNote.getUuid());
// assertEquals("Updated Name", foundNote.getName());
// assertEquals("Updated content", foundNote.getText());
// }
//
// @Test
// void updateNonExistingNote_ShouldThrowException() {
// // Given
// UUID nonExistingId = UUID.randomUUID();
// NoteRequest updateRequest = new NoteRequest();
// updateRequest.setName("Update Name");
// updateRequest.setText("Update content");
//
// // When & Then
// ResponseStatusException exception = assertThrows(
// ResponseStatusException.class,
// () -> noteService.update(nonExistingId, updateRequest)
// );
//
// assertTrue(exception.getMessage().contains("Note not found"));
// }
//
// @Test
// void deleteExistingNote() {
// // Given
// NoteRequest noteRequest = new NoteRequest();
// noteRequest.setName("Delete Me");
// noteRequest.setText("Delete content");
// NoteResponse savedNote = noteService.add(noteRequest);
//
// // When
// assertDoesNotThrow(() -> noteService.delete(savedNote.getUuid()));
//
// // Then
// List<NoteResponse> notes = noteService.getAll();
// assertTrue(notes.isEmpty());
//
// // Проверяем, что заметка действительно удалена
// ResponseStatusException exception = assertThrows(
// ResponseStatusException.class,
// () -> noteService.find(savedNote.getUuid())
// );
// assertTrue(exception.getMessage().contains("Note not found"));
// }
//
// @Test
// void deleteNonExistingNote_ShouldThrowException() {
// // Given
// UUID nonExistingId = UUID.randomUUID();
//
// // When & Then
// ResponseStatusException exception = assertThrows(
// ResponseStatusException.class,
// () -> noteService.delete(nonExistingId)
// );
//
// assertTrue(exception.getMessage().contains("Note not found"));
// }
//
// @Test
// void getAllNotes_WhenEmpty() {
// // When
// List<NoteResponse> notes = noteService.getAll();
//
// // Then
// assertNotNull(notes);
// assertTrue(notes.isEmpty());
// }
//
// @Test
// void getAllNotes_WithMultipleNotes() {
// // Given
// for (int i = 1; i <= 5; i++) {
// NoteRequest noteRequest = new NoteRequest();
// noteRequest.setName("Note " + i);
// noteRequest.setText("Content for note " + i);
// noteService.add(noteRequest);
// }
//
// // When
// List<NoteResponse> notes = noteService.getAll();
//
// // Then
// assertNotNull(notes);
// assertEquals(5, notes.size());
// }
}