diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43a564b --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# 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 + +# IDEs +.idea/ +*.iws +*.iml +*.ipr +.vscode/ +.classpath +.project +.settings/ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log + +# Other +.env +.env.local +.env.*.local \ No newline at end of file diff --git a/README.md b/README.md index a6bbd59..5c1d531 100644 --- a/README.md +++ b/README.md @@ -1 +1,104 @@ -# coding-agent-output-comparison \ No newline at end of file +# User Management REST API + +REST API for user management with JWT authentication based on the provided specifications. + +## Features + +- **Endpoint:** `GET /api/users/{id}` +- **Authentication:** JWT token required +- **Authorization:** Admin role only +- **Response:** JSON with user data (id, name, email, role) +- **Error Handling:** Proper HTTP status codes (200, 401, 403, 404) + +## Technical Stack + +- **Framework:** Spring Boot 3.1.5 +- **Java Version:** 17 +- **Security:** JWT tokens with HMAC-SHA384 signing +- **Database:** H2 in-memory +- **ORM:** JPA/Hibernate + +## Quick Start + +### 1. Build and Run + +```bash +# Build and run the application +mvn spring-boot:run +``` + +The API will start on `http://localhost:8080` + +### 2. Generate JWT Tokens + +```bash +# Generate test tokens +mvn spring-boot:run -Dspring-boot.run.profiles=token-gen +``` + +This will output admin and user tokens for testing. + +### 3. Test API Endpoints + +```bash +# Test with admin token (replace {admin-token} with actual token) +curl -H "Authorization: Bearer {admin-token}" http://localhost:8080/api/users/1 + +# Test without token (should return 403) +curl http://localhost:8080/api/users/1 + +# Test with non-existent user (should return 404) +curl -H "Authorization: Bearer {admin-token}" http://localhost:8080/api/users/999 +``` + +## API Documentation + +### GET /api/users/{id} + +Retrieve user by ID (Admin only). + +**Parameters:** +- `id` (path) - User ID (Long, required) + +**Headers:** +- `Authorization: Bearer {jwt-token}` (required) + +**Responses:** + +- **200 OK** - User found and returned +```json +{ + "id": 1, + "name": "Admin User", + "email": "admin@example.com", + "role": "ADMIN" +} +``` + +- **401 Unauthorized** - Missing or invalid JWT token +- **403 Forbidden** - Insufficient permissions (non-admin user) +- **404 Not Found** - User not found +```json +{ + "error": "Not Found", + "message": "User not found with id: 999", + "timestamp": "2025-09-02T07:00:00.000", + "status": 404 +} +``` + +## Test Data + +The application automatically loads test data: + +1. **ID: 1** - Admin User (admin@example.com, ADMIN) +2. **ID: 2** - Regular User (user@example.com, USER) +3. **ID: 3** - Test User (test@example.com, USER) + +## Database Console + +For development, H2 console is available at: `http://localhost:8080/h2-console` + +- **JDBC URL:** `jdbc:h2:mem:testdb` +- **Username:** `sa` +- **Password:** (empty) \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ba8696b --- /dev/null +++ b/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + + + com.example + user-api + 1.0.0 + jar + + User API + REST API for user management with JWT authentication + + + 17 + 0.11.5 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + + + io.jsonwebtoken + jjwt-api + ${jwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jwt.version} + 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..d11cecc --- /dev/null +++ b/src/main/java/com/example/userapi/UserApiApplication.java @@ -0,0 +1,11 @@ +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/config/DataLoader.java b/src/main/java/com/example/userapi/config/DataLoader.java new file mode 100644 index 0000000..e7ce084 --- /dev/null +++ b/src/main/java/com/example/userapi/config/DataLoader.java @@ -0,0 +1,26 @@ +package com.example.userapi.config; + +import com.example.userapi.entity.User; +import com.example.userapi.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("!test") +public class DataLoader implements CommandLineRunner { + + @Autowired + private UserRepository userRepository; + + @Override + public void run(String... args) throws Exception { + // Only load data if the repository is empty + if (userRepository.count() == 0) { + userRepository.save(new User("Admin User", "admin@example.com", "ADMIN")); + userRepository.save(new User("Regular User", "user@example.com", "USER")); + userRepository.save(new User("Test User", "test@example.com", "USER")); + } + } +} \ 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..6671ec6 --- /dev/null +++ b/src/main/java/com/example/userapi/controller/UserController.java @@ -0,0 +1,34 @@ +package com.example.userapi.controller; + +import com.example.userapi.dto.UserResponse; +import com.example.userapi.entity.User; +import com.example.userapi.exception.UserNotFoundException; +import com.example.userapi.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + @GetMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getUserById(@PathVariable Long id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new UserNotFoundException(id)); + + UserResponse response = new UserResponse( + user.getId(), + user.getName(), + user.getEmail(), + user.getRole() + ); + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/dto/UserResponse.java b/src/main/java/com/example/userapi/dto/UserResponse.java new file mode 100644 index 0000000..b0dc4b5 --- /dev/null +++ b/src/main/java/com/example/userapi/dto/UserResponse.java @@ -0,0 +1,51 @@ +package com.example.userapi.dto; + +public class UserResponse { + private Long id; + private String name; + private String email; + private String role; + + // Constructors + public UserResponse() {} + + public UserResponse(Long id, String name, String email, String role) { + this.id = id; + 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/entity/User.java b/src/main/java/com/example/userapi/entity/User.java new file mode 100644 index 0000000..008a576 --- /dev/null +++ b/src/main/java/com/example/userapi/entity/User.java @@ -0,0 +1,69 @@ +package com.example.userapi.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @Column(nullable = false) + private String name; + + @Email + @NotBlank + @Column(nullable = false, unique = true) + private String email; + + @NotBlank + @Column(nullable = false) + private String role; + + // Constructors + public User() {} + + 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..10d42cc --- /dev/null +++ b/src/main/java/com/example/userapi/exception/GlobalExceptionHandler.java @@ -0,0 +1,49 @@ +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 java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity> handleUserNotFoundException(UserNotFoundException ex) { + Map errorResponse = new HashMap<>(); + errorResponse.put("timestamp", LocalDateTime.now()); + errorResponse.put("status", HttpStatus.NOT_FOUND.value()); + errorResponse.put("error", "Not Found"); + errorResponse.put("message", ex.getMessage()); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity> handleAccessDeniedException(AccessDeniedException ex) { + Map errorResponse = new HashMap<>(); + errorResponse.put("timestamp", LocalDateTime.now()); + errorResponse.put("status", HttpStatus.FORBIDDEN.value()); + errorResponse.put("error", "Forbidden"); + errorResponse.put("message", "Insufficient permissions"); + + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity> handleBadCredentialsException(BadCredentialsException ex) { + Map errorResponse = new HashMap<>(); + errorResponse.put("timestamp", LocalDateTime.now()); + errorResponse.put("status", HttpStatus.UNAUTHORIZED.value()); + errorResponse.put("error", "Unauthorized"); + errorResponse.put("message", "Authentication failed"); + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); + } +} \ 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..402bb0d --- /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(Long id) { + super("User not found with id: " + id); + } +} \ 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..afde6a8 --- /dev/null +++ b/src/main/java/com/example/userapi/security/JwtAuthenticationFilter.java @@ -0,0 +1,49 @@ +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 authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + + if (jwtUtil.validateToken(token)) { + String username = jwtUtil.getUsernameFromToken(token); + String role = jwtUtil.getRoleFromToken(token); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + username, + null, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) + ); + + SecurityContextHolder.getContext().setAuthentication(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..ec0805f --- /dev/null +++ b/src/main/java/com/example/userapi/security/JwtUtil.java @@ -0,0 +1,68 @@ +package com.example.userapi.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:mySecretKey12345678901234567890}") + private String jwtSecret; + + @Value("${jwt.expiration:86400000}") + private long jwtExpiration; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(jwtSecret.getBytes()); + } + + public String generateToken(String username, String role) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtExpiration); + + return Jwts.builder() + .setSubject(username) + .claim("role", role) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public String getUsernameFromToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + return claims.getSubject(); + } + + public String getRoleFromToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + return claims.get("role", String.class); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} \ 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..8818c7d --- /dev/null +++ b/src/main/java/com/example/userapi/security/SecurityConfig.java @@ -0,0 +1,39 @@ +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.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +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(authz -> authz + .requestMatchers(new AntPathRequestMatcher("/api/users/**")).hasRole("ADMIN") + .requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll() // For testing + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + // For H2 console access (testing only) + http.headers(headers -> headers + .frameOptions(frameOptions -> frameOptions.sameOrigin())); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/userapi/util/TokenGenerator.java b/src/main/java/com/example/userapi/util/TokenGenerator.java new file mode 100644 index 0000000..1ec26f3 --- /dev/null +++ b/src/main/java/com/example/userapi/util/TokenGenerator.java @@ -0,0 +1,27 @@ +package com.example.userapi.util; + +import com.example.userapi.security.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("token-gen") +public class TokenGenerator implements CommandLineRunner { + + @Autowired + private JwtUtil jwtUtil; + + @Override + public void run(String... args) throws Exception { + String adminToken = jwtUtil.generateToken("admin@example.com", "ADMIN"); + String userToken = jwtUtil.generateToken("user@example.com", "USER"); + + System.out.println("Admin Token:"); + System.out.println(adminToken); + System.out.println(); + System.out.println("User Token:"); + System.out.println(userToken); + } +} \ 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..fa78775 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,29 @@ +# Server Configuration +server.port=8080 + +# Database Configuration (H2 for testing) +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.properties.hibernate.format_sql=true + +# Disable automatic SQL script execution +spring.sql.init.mode=never + +# H2 Console (for testing) +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# JWT Configuration +jwt.secret=mySecretKey12345678901234567890123456789012345678901234567890 +jwt.expiration=86400000 + +# Logging +logging.level.com.example.userapi=DEBUG +logging.level.org.springframework.security=DEBUG \ No newline at end of file diff --git a/src/test/java/com/example/userapi/UserApiApplicationTest.java b/src/test/java/com/example/userapi/UserApiApplicationTest.java new file mode 100644 index 0000000..c6af287 --- /dev/null +++ b/src/test/java/com/example/userapi/UserApiApplicationTest.java @@ -0,0 +1,15 @@ +package com.example.userapi; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class UserApiApplicationTest { + + @Test + void contextLoads() { + // This test ensures that the Spring context loads successfully + } +} \ 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..873eba4 --- /dev/null +++ b/src/test/java/com/example/userapi/controller/UserControllerTest.java @@ -0,0 +1,114 @@ +package com.example.userapi.controller; + +import com.example.userapi.entity.User; +import com.example.userapi.repository.UserRepository; +import com.example.userapi.security.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@ActiveProfiles("test") +class UserControllerTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private ObjectMapper objectMapper; + + private MockMvc mockMvc; + private String adminToken; + private String userToken; + private User testUser; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + userRepository.deleteAll(); + + // Create test users + testUser = new User("Test User", "test@example.com", "USER"); + testUser = userRepository.save(testUser); + + User adminUser = new User("Admin User", "admin@example.com", "ADMIN"); + adminUser = userRepository.save(adminUser); + + // Generate tokens + adminToken = jwtUtil.generateToken("admin@example.com", "ADMIN"); + userToken = jwtUtil.generateToken("user@example.com", "USER"); + } + + @Test + void getUserById_WithAdminToken_ShouldReturnUser() throws Exception { + mockMvc.perform(get("/api/users/{id}", testUser.getId()) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(testUser.getId())) + .andExpect(jsonPath("$.name").value("Test User")) + .andExpect(jsonPath("$.email").value("test@example.com")) + .andExpect(jsonPath("$.role").value("USER")); + } + + @Test + void getUserById_WithNonAdminToken_ShouldReturnForbidden() throws Exception { + mockMvc.perform(get("/api/users/{id}", testUser.getId()) + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(403)) + .andExpect(jsonPath("$.error").value("Forbidden")); + } + + @Test + void getUserById_WithoutToken_ShouldReturnUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/{id}", testUser.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + void getUserById_WithInvalidToken_ShouldReturnUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/{id}", testUser.getId()) + .header("Authorization", "Bearer invalid-token") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + void getUserById_WithNonExistentUserId_ShouldReturnNotFound() throws Exception { + Long nonExistentId = 999L; + + mockMvc.perform(get("/api/users/{id}", nonExistentId) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(404)) + .andExpect(jsonPath("$.error").value("Not Found")) + .andExpect(jsonPath("$.message").value("User not found with id: " + nonExistentId)); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/userapi/security/JwtUtilTest.java b/src/test/java/com/example/userapi/security/JwtUtilTest.java new file mode 100644 index 0000000..69ec293 --- /dev/null +++ b/src/test/java/com/example/userapi/security/JwtUtilTest.java @@ -0,0 +1,68 @@ +package com.example.userapi.security; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles("test") +class JwtUtilTest { + + @Autowired + private JwtUtil jwtUtil; + + @Test + void generateToken_ShouldCreateValidToken() { + String username = "test@example.com"; + String role = "ADMIN"; + + String token = jwtUtil.generateToken(username, role); + + assertNotNull(token); + assertFalse(token.isEmpty()); + assertTrue(jwtUtil.validateToken(token)); + } + + @Test + void getUsernameFromToken_ShouldReturnCorrectUsername() { + String username = "test@example.com"; + String role = "ADMIN"; + String token = jwtUtil.generateToken(username, role); + + String extractedUsername = jwtUtil.getUsernameFromToken(token); + + assertEquals(username, extractedUsername); + } + + @Test + void getRoleFromToken_ShouldReturnCorrectRole() { + String username = "test@example.com"; + String role = "ADMIN"; + String token = jwtUtil.generateToken(username, role); + + String extractedRole = jwtUtil.getRoleFromToken(token); + + assertEquals(role, extractedRole); + } + + @Test + void validateToken_WithInvalidToken_ShouldReturnFalse() { + String invalidToken = "invalid.token.here"; + + boolean isValid = jwtUtil.validateToken(invalidToken); + + assertFalse(isValid); + } + + @Test + void validateToken_WithEmptyToken_ShouldReturnFalse() { + String emptyToken = ""; + + boolean isValid = jwtUtil.validateToken(emptyToken); + + assertFalse(isValid); + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..65a0671 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,25 @@ +# Test Configuration +spring.profiles.active=test + +# Database Configuration (H2 for testing) +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +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=false +spring.h2.console.enabled=false + +# Disable automatic SQL script execution in tests +spring.sql.init.mode=never + +# JWT Configuration +jwt.secret=testSecretKey12345678901234567890123456789012345678901234567890 +jwt.expiration=86400000 + +# Logging +logging.level.com.example.userapi=INFO +logging.level.org.springframework.security=WARN \ No newline at end of file