diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df2addd --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# IDE +.idea/ +*.iml +.vscode/ +.settings/ +.project +.classpath + +# OS +.DS_Store +Thumbs.db + +# Spring Boot +*.tmp +*.bak \ No newline at end of file diff --git a/README.md b/README.md index a6bbd59..da66585 100644 --- a/README.md +++ b/README.md @@ -1 +1,142 @@ -# coding-agent-output-comparison \ No newline at end of file +# User API - REST API for User Management + +A Spring Boot REST API implementation based on detailed specifications for user management with JWT authentication. + +## Features + +- **GET /api/users/{id}** - Retrieve user by ID +- JWT-based authentication and authorization +- Input validation for path parameters +- Comprehensive error handling (400, 401, 403, 404) +- H2 in-memory database with JPA/Hibernate +- Complete test coverage + +## API Specification + +### Endpoint +- **URL**: `GET /api/users/{id}` +- **Method**: GET +- **Path Parameter**: `id` (Long, 0以上, required) +- **Authentication**: JWT Bearer token required + +### Request Example +```bash +GET /api/users/123 +Authorization: Bearer {JWT} +``` + +### Response Examples + +#### Success (200 OK) +```json +{ + "id": 123, + "name": "田中 太郎", + "email": "tanaka@example.com", + "role": "admin" +} +``` + +#### Error Responses +| Status | Response | Description | +|--------|----------|-------------| +| 404 | `{"error": "User not found"}` | User does not exist | +| 401 | `{"error": "Unauthorized"}` | Authentication failed | +| 403 | `{"error": "Forbidden"}` | Insufficient permissions | +| 400 | `{"error": "Invalid ID"}` | Invalid ID format or negative value | + +## Database Schema + +```sql +CREATE TABLE users ( + id BIGINT PRIMARY KEY, + name VARCHAR(100), + email VARCHAR(255), + role VARCHAR(20) +); +``` + +## Running the Application + +### Prerequisites +- Java 17+ +- Maven 3.6+ + +### Start the Application +```bash +mvn spring-boot:run +``` + +The application will start on `http://localhost:8080` + +### Generate JWT Token (for testing) +```bash +curl -X POST "http://localhost:8080/auth/token" \ + -H "Content-Type: application/json" \ + -d '{"username": "testuser", "role": "admin"}' +``` + +### Test the API +```bash +# Get JWT token +TOKEN=$(curl -s -X POST "http://localhost:8080/auth/token" \ + -H "Content-Type: application/json" \ + -d '{"username": "testuser", "role": "admin"}' | jq -r '.token') + +# Test successful request +curl -X GET "http://localhost:8080/api/users/1" \ + -H "Authorization: Bearer $TOKEN" + +# Test user not found +curl -X GET "http://localhost:8080/api/users/999" \ + -H "Authorization: Bearer $TOKEN" + +# Test unauthorized access +curl -X GET "http://localhost:8080/api/users/1" + +# Test invalid ID +curl -X GET "http://localhost:8080/api/users/abc" \ + -H "Authorization: Bearer $TOKEN" +``` + +## Running Tests + +```bash +mvn test +``` + +All tests validate the API behavior including: +- Successful user retrieval +- User not found scenarios +- Authentication and authorization +- Input validation +- Error handling + +## Technology Stack + +- **Spring Boot 3.2.0** - Application framework +- **Spring Security** - Authentication and authorization +- **Spring Data JPA** - Database access layer +- **H2 Database** - In-memory database +- **JWT (JJWT)** - JSON Web Token implementation +- **JUnit 5** - Testing framework + +## Project Structure + +``` +src/ +├── main/ +│ ├── java/com/example/userapi/ +│ │ ├── controller/ # REST controllers +│ │ ├── entity/ # JPA entities +│ │ ├── exception/ # Custom exceptions and handlers +│ │ ├── repository/ # Data repositories +│ │ ├── security/ # Security configuration and JWT utilities +│ │ └── UserApiApplication.java +│ └── resources/ +│ ├── application.properties +│ └── data.sql # Sample data +└── test/ + └── java/com/example/userapi/ + └── controller/ # API tests +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..43f629e --- /dev/null +++ b/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + com.example + user-api + 0.0.1-SNAPSHOT + user-api + REST API for user management with JWT authentication + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/src/main/java/com/example/userapi/UserApiApplication.java b/src/main/java/com/example/userapi/UserApiApplication.java new file mode 100644 index 0000000..0106599 --- /dev/null +++ b/src/main/java/com/example/userapi/UserApiApplication.java @@ -0,0 +1,13 @@ +package com.example.userapi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserApiApplication { + + public static void main(String[] args) { + SpringApplication.run(UserApiApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/controller/AuthController.java b/src/main/java/com/example/userapi/controller/AuthController.java new file mode 100644 index 0000000..c35b1cf --- /dev/null +++ b/src/main/java/com/example/userapi/controller/AuthController.java @@ -0,0 +1,24 @@ +package com.example.userapi.controller; + +import com.example.userapi.security.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Autowired + private JwtUtil jwtUtil; + + @PostMapping("/token") + public Map generateToken(@RequestBody Map credentials) { + String username = credentials.get("username"); + String role = credentials.getOrDefault("role", "user"); + + String token = jwtUtil.generateToken(username, role); + return Map.of("token", token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/controller/UserController.java b/src/main/java/com/example/userapi/controller/UserController.java new file mode 100644 index 0000000..ac9b482 --- /dev/null +++ b/src/main/java/com/example/userapi/controller/UserController.java @@ -0,0 +1,28 @@ +package com.example.userapi.controller; + +import com.example.userapi.entity.User; +import com.example.userapi.exception.InvalidIdException; +import com.example.userapi.exception.UserNotFoundException; +import com.example.userapi.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + @GetMapping("/{id}") + public User getUserById(@PathVariable Long id) { + // Validate ID (must be 0 or greater) + if (id < 0) { + throw new InvalidIdException("Invalid ID"); + } + + // Find user in database + return userRepository.findById(id) + .orElseThrow(() -> new UserNotFoundException("User not found")); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/entity/User.java b/src/main/java/com/example/userapi/entity/User.java new file mode 100644 index 0000000..58f514f --- /dev/null +++ b/src/main/java/com/example/userapi/entity/User.java @@ -0,0 +1,65 @@ +package com.example.userapi.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name", length = 100) + private String name; + + @Column(name = "email", length = 255) + private String email; + + @Column(name = "role", length = 20) + private String role; + + // Default constructor + public User() { + } + + // Constructor with all fields + public User(String name, String email, String role) { + this.name = name; + this.email = email; + this.role = role; + } + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/exception/GlobalExceptionHandler.java b/src/main/java/com/example/userapi/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..6a39a4c --- /dev/null +++ b/src/main/java/com/example/userapi/exception/GlobalExceptionHandler.java @@ -0,0 +1,45 @@ +package com.example.userapi.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity> handleUserNotFound(UserNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", "User not found")); + } + + @ExceptionHandler(InvalidIdException.class) + public ResponseEntity> handleInvalidId(InvalidIdException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", "Invalid ID")); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", "Invalid ID")); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity> handleUnauthorized(BadCredentialsException ex) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(Map.of("error", "Unauthorized")); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity> handleForbidden(AccessDeniedException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(Map.of("error", "Forbidden")); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/exception/InvalidIdException.java b/src/main/java/com/example/userapi/exception/InvalidIdException.java new file mode 100644 index 0000000..5aac521 --- /dev/null +++ b/src/main/java/com/example/userapi/exception/InvalidIdException.java @@ -0,0 +1,7 @@ +package com.example.userapi.exception; + +public class InvalidIdException extends RuntimeException { + public InvalidIdException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/exception/UserNotFoundException.java b/src/main/java/com/example/userapi/exception/UserNotFoundException.java new file mode 100644 index 0000000..f2f613c --- /dev/null +++ b/src/main/java/com/example/userapi/exception/UserNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.userapi.exception; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/repository/UserRepository.java b/src/main/java/com/example/userapi/repository/UserRepository.java new file mode 100644 index 0000000..c7bd0fa --- /dev/null +++ b/src/main/java/com/example/userapi/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.example.userapi.repository; + +import com.example.userapi.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/security/JwtAuthenticationFilter.java b/src/main/java/com/example/userapi/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..cfebb85 --- /dev/null +++ b/src/main/java/com/example/userapi/security/JwtAuthenticationFilter.java @@ -0,0 +1,54 @@ +package com.example.userapi.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String authorizationHeader = request.getHeader("Authorization"); + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + String jwt = authorizationHeader.substring(7); + + try { + if (jwtUtil.validateToken(jwt)) { + String username = jwtUtil.extractUsername(jwt); + String role = jwtUtil.extractRole(jwt); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + username, + null, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + } catch (Exception e) { + // Invalid token, continue without authentication + } + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/security/JwtUtil.java b/src/main/java/com/example/userapi/security/JwtUtil.java new file mode 100644 index 0000000..7e3b4e0 --- /dev/null +++ b/src/main/java/com/example/userapi/security/JwtUtil.java @@ -0,0 +1,69 @@ +package com.example.userapi.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtUtil { + + private static final String SECRET = "mySecretKeyForJWTTokenGenerationThatIsLongEnoughForHS256Algorithm"; + private static final int JWT_EXPIRATION = 86400000; // 24 hours + + private final SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes()); + + public String generateToken(String username, String role) { + return Jwts.builder() + .setSubject(username) + .claim("role", role) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public String extractRole(String token) { + return extractClaim(token, claims -> claims.get("role", String.class)); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, ClaimsResolver claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.resolve(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public Boolean validateToken(String token) { + try { + return !isTokenExpired(token); + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + @FunctionalInterface + public interface ClaimsResolver { + T resolve(Claims claims); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/security/SecurityConfig.java b/src/main/java/com/example/userapi/security/SecurityConfig.java new file mode 100644 index 0000000..88922c1 --- /dev/null +++ b/src/main/java/com/example/userapi/security/SecurityConfig.java @@ -0,0 +1,43 @@ +package com.example.userapi.security; + +import org.springframework.beans.factory.annotation.Autowired; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/users/**").authenticated() + .anyRequest().permitAll() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(401); + response.setContentType("application/json"); + response.getWriter().write("{\"error\": \"Unauthorized\"}"); + }) + .accessDeniedHandler((request, response, accessDeniedException) -> { + response.setStatus(403); + response.setContentType("application/json"); + response.getWriter().write("{\"error\": \"Forbidden\"}"); + }) + ); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a76ca10 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,17 @@ +# Database configuration (H2 in-memory for demo) +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# JPA configuration +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.defer-datasource-initialization=true + +# H2 Console (for testing) +spring.h2.console.enabled=true + +# Server configuration +server.port=8080 \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..fff51bd --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,3 @@ +INSERT INTO users (name, email, role) VALUES ('田中 太郎', 'tanaka@example.com', 'admin'); +INSERT INTO users (name, email, role) VALUES ('佐藤 花子', 'sato@example.com', 'user'); +INSERT INTO users (name, email, role) VALUES ('山田 次郎', 'yamada@example.com', 'moderator'); \ No newline at end of file diff --git a/src/test/java/com/example/userapi/controller/UserControllerTest.java b/src/test/java/com/example/userapi/controller/UserControllerTest.java new file mode 100644 index 0000000..79f4822 --- /dev/null +++ b/src/test/java/com/example/userapi/controller/UserControllerTest.java @@ -0,0 +1,126 @@ +package com.example.userapi.controller; + +import com.example.userapi.entity.User; +import com.example.userapi.repository.UserRepository; +import com.example.userapi.security.JwtUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class UserControllerTest { + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtUtil jwtUtil; + + private String validJwtToken; + private String baseUrl; + + @BeforeEach + void setUp() { + baseUrl = "http://localhost:" + port + "/api/users/"; + // Create a valid JWT token for testing + validJwtToken = jwtUtil.generateToken("testuser", "admin"); + + // Add test user + User testUser = new User("田中 太郎", "tanaka@example.com", "admin"); + userRepository.save(testUser); + } + + @Test + void testGetUserById_Success() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + validJwtToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + baseUrl + "1", HttpMethod.GET, entity, String.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.getBody().contains("田中 太郎")); + assertTrue(response.getBody().contains("tanaka@example.com")); + assertTrue(response.getBody().contains("admin")); + } + + @Test + void testGetUserById_UserNotFound() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + validJwtToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + baseUrl + "999", HttpMethod.GET, entity, String.class); + + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + assertTrue(response.getBody().contains("User not found")); + } + + @Test + void testGetUserById_InvalidId() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + validJwtToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + baseUrl + "-1", HttpMethod.GET, entity, String.class); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertTrue(response.getBody().contains("Invalid ID")); + } + + @Test + void testGetUserById_InvalidIdFormat() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + validJwtToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + baseUrl + "abc", HttpMethod.GET, entity, String.class); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertTrue(response.getBody().contains("Invalid ID")); + } + + @Test + void testGetUserById_Unauthorized() throws Exception { + ResponseEntity response = restTemplate.exchange( + baseUrl + "1", HttpMethod.GET, null, String.class); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertTrue(response.getBody().contains("Unauthorized")); + } + + @Test + void testGetUserById_InvalidToken() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer invalid-token"); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + baseUrl + "1", HttpMethod.GET, entity, String.class); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertTrue(response.getBody().contains("Unauthorized")); + } +} \ No newline at end of file