refactor: remove Phase 1 auth endpoints, switch to Logto OIDC

Auth is now handled by Logto. Removed AuthController, AuthService,
and related DTOs. Integration tests use Spring Security JWT mocks.
Ed25519 JwtService retained for machine token signing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-04 15:12:50 +02:00
parent ab9ad1ab7f
commit db7647f7f4
12 changed files with 34 additions and 515 deletions

View File

@@ -1,54 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import net.siegeln.cameleer.saas.auth.dto.AuthResponse;
import net.siegeln.cameleer.saas.auth.dto.LoginRequest;
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request,
HttpServletRequest httpRequest) {
try {
var response = authService.register(request, extractClientIp(httpRequest));
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).build();
}
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request,
HttpServletRequest httpRequest) {
try {
var response = authService.login(request, extractClientIp(httpRequest));
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
private String extractClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}

View File

@@ -1,82 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import net.siegeln.cameleer.saas.audit.AuditAction;
import net.siegeln.cameleer.saas.audit.AuditService;
import net.siegeln.cameleer.saas.auth.dto.AuthResponse;
import net.siegeln.cameleer.saas.auth.dto.LoginRequest;
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class AuthService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuditService auditService;
public AuthService(UserRepository userRepository,
RoleRepository roleRepository,
PasswordEncoder passwordEncoder,
JwtService jwtService,
AuditService auditService) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
this.auditService = auditService;
}
public AuthResponse register(RegisterRequest request, String sourceIp) {
if (userRepository.existsByEmail(request.email())) {
throw new IllegalArgumentException("Email already registered");
}
var user = new UserEntity();
user.setEmail(request.email());
user.setName(request.name());
user.setPassword(passwordEncoder.encode(request.password()));
roleRepository.findByName("OWNER").ifPresent(role -> user.getRoles().add(role));
var saved = userRepository.save(user);
var token = jwtService.generateToken(saved);
auditService.log(
saved.getId(), saved.getEmail(), null,
AuditAction.AUTH_REGISTER, null,
null, sourceIp,
"SUCCESS", null
);
return new AuthResponse(token, saved.getEmail(), saved.getName());
}
public AuthResponse login(LoginRequest request, String sourceIp) {
var user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new IllegalArgumentException("Invalid credentials"));
if (!passwordEncoder.matches(request.password(), user.getPassword())) {
auditService.log(
user.getId(), user.getEmail(), null,
AuditAction.AUTH_LOGIN_FAILED, null,
null, sourceIp,
"FAILURE", null
);
throw new IllegalArgumentException("Invalid credentials");
}
var token = jwtService.generateToken(user);
auditService.log(
user.getId(), user.getEmail(), null,
AuditAction.AUTH_LOGIN, null,
null, sourceIp,
"SUCCESS", null
);
return new AuthResponse(token, user.getEmail(), user.getName());
}
}

View File

@@ -1,8 +0,0 @@
package net.siegeln.cameleer.saas.auth.dto;
public record AuthResponse(
String token,
String email,
String name
) {
}

View File

@@ -1,10 +0,0 @@
package net.siegeln.cameleer.saas.auth.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record LoginRequest(
@NotBlank @Email String email,
@NotBlank String password
) {
}

View File

@@ -1,12 +0,0 @@
package net.siegeln.cameleer.saas.auth.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record RegisterRequest(
@NotBlank @Email String email,
@NotBlank String name,
@NotBlank @Size(min = 8, max = 128) String password
) {
}

View File

@@ -47,7 +47,6 @@ public class SecurityConfig {
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll() .requestMatchers("/actuator/health").permitAll()
.requestMatchers("/auth/verify").permitAll() .requestMatchers("/auth/verify").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()

View File

@@ -32,8 +32,14 @@ public class LicenseController {
var tenant = tenantService.getById(tenantId).orElse(null); var tenant = tenantService.getById(tenantId).orElse(null);
if (tenant == null) return ResponseEntity.notFound().build(); if (tenant == null) return ResponseEntity.notFound().build();
UUID actorId = authentication.getCredentials() instanceof UUID uid // Extract actor ID from JWT subject (Logto OIDC: sub may be a non-UUID string)
? uid : UUID.fromString(authentication.getCredentials().toString()); String sub = authentication.getName();
UUID actorId;
try {
actorId = UUID.fromString(sub);
} catch (IllegalArgumentException e) {
actorId = UUID.nameUUIDFromBytes(sub.getBytes());
}
var license = licenseService.generateLicense(tenant, Duration.ofDays(365), actorId); var license = licenseService.generateLicense(tenant, Duration.ofDays(365), actorId);
return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(license)); return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(license));

View File

@@ -29,9 +29,14 @@ public class TenantController {
public ResponseEntity<TenantResponse> create(@Valid @RequestBody CreateTenantRequest request, public ResponseEntity<TenantResponse> create(@Valid @RequestBody CreateTenantRequest request,
Authentication authentication) { Authentication authentication) {
try { try {
// Extract actor ID from authentication credentials (Phase 1: userId stored as credentials) // Extract actor ID from JWT subject (Logto OIDC: sub may be a non-UUID string)
UUID actorId = authentication.getCredentials() instanceof UUID uid String sub = authentication.getName();
? uid : UUID.fromString(authentication.getCredentials().toString()); UUID actorId;
try {
actorId = UUID.fromString(sub);
} catch (IllegalArgumentException e) {
actorId = UUID.nameUUIDFromBytes(sub.getBytes());
}
var entity = tenantService.create(request, actorId); var entity = tenantService.create(request, actorId);
return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(entity)); return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(entity));

View File

@@ -1,126 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.siegeln.cameleer.saas.TestcontainersConfig;
import net.siegeln.cameleer.saas.TestSecurityConfig;
import net.siegeln.cameleer.saas.auth.dto.LoginRequest;
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@Import({TestcontainersConfig.class, TestSecurityConfig.class})
@ActiveProfiles("test")
class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void register_returns201WithToken() throws Exception {
var request = new RegisterRequest("newuser@example.com", "New User", "password123");
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.token").isNotEmpty())
.andExpect(jsonPath("$.email").value("newuser@example.com"))
.andExpect(jsonPath("$.name").value("New User"));
}
@Test
void register_returns409ForDuplicateEmail() throws Exception {
var request = new RegisterRequest("duplicate@example.com", "User One", "password123");
// First registration
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated());
// Duplicate registration
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict());
}
@Test
void login_returns200WithToken() throws Exception {
var registerRequest = new RegisterRequest("loginuser@example.com", "Login User", "password123");
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isCreated());
var loginRequest = new LoginRequest("loginuser@example.com", "password123");
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").isNotEmpty())
.andExpect(jsonPath("$.email").value("loginuser@example.com"));
}
@Test
void login_returns401ForBadPassword() throws Exception {
var registerRequest = new RegisterRequest("badpass@example.com", "Bad Pass", "password123");
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isCreated());
var loginRequest = new LoginRequest("badpass@example.com", "wrong-password");
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isUnauthorized());
}
@Test
void protectedEndpoint_returns401WithoutToken() throws Exception {
mockMvc.perform(get("/api/health/secured"))
.andExpect(status().isUnauthorized());
}
@Test
void protectedEndpoint_returns200WithValidToken() throws Exception {
// Register to get a token
var registerRequest = new RegisterRequest("secured@example.com", "Secured User", "password123");
var result = mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isCreated())
.andReturn();
var responseBody = objectMapper.readTree(result.getResponse().getContentAsString());
String token = responseBody.get("token").asText();
// Access protected endpoint with token
mockMvc.perform(get("/api/health/secured")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("authenticated"));
}
}

View File

@@ -1,171 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import net.siegeln.cameleer.saas.audit.AuditAction;
import net.siegeln.cameleer.saas.audit.AuditService;
import net.siegeln.cameleer.saas.auth.dto.LoginRequest;
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AuthServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private RoleRepository roleRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtService jwtService;
@Mock
private AuditService auditService;
private AuthService authService;
@BeforeEach
void setUp() {
authService = new AuthService(userRepository, roleRepository,
passwordEncoder, jwtService, auditService);
}
@Test
void register_createsUserAndReturnsToken() {
var request = new RegisterRequest("user@example.com", "Test User", "password123");
var ownerRole = new RoleEntity();
ownerRole.setName("OWNER");
when(userRepository.existsByEmail("user@example.com")).thenReturn(false);
when(passwordEncoder.encode("password123")).thenReturn("encoded-password");
when(roleRepository.findByName("OWNER")).thenReturn(Optional.of(ownerRole));
when(userRepository.save(any(UserEntity.class))).thenAnswer(invocation -> {
UserEntity user = invocation.getArgument(0);
// simulate ID assignment by persistence
try {
var idField = UserEntity.class.getDeclaredField("id");
idField.setAccessible(true);
idField.set(user, java.util.UUID.randomUUID());
} catch (Exception e) {
throw new RuntimeException(e);
}
return user;
});
when(jwtService.generateToken(any(UserEntity.class))).thenReturn("test-jwt-token");
var response = authService.register(request, "127.0.0.1");
assertNotNull(response);
assertEquals("test-jwt-token", response.token());
assertEquals("user@example.com", response.email());
assertEquals("Test User", response.name());
// Verify audit was logged
verify(auditService).log(
any(), eq("user@example.com"), eq(null),
eq(AuditAction.AUTH_REGISTER), eq(null),
eq(null), eq("127.0.0.1"),
eq("SUCCESS"), eq(null)
);
}
@Test
void register_rejectsDuplicateEmail() {
var request = new RegisterRequest("existing@example.com", "Test User", "password123");
when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
var exception = assertThrows(IllegalArgumentException.class,
() -> authService.register(request, "127.0.0.1"));
assertEquals("Email already registered", exception.getMessage());
verify(userRepository, never()).save(any());
verify(auditService, never()).log(any(), any(), any(), any(), any(), any(), any(), any(), any());
}
@Test
void login_returnsTokenForValidCredentials() {
var request = new LoginRequest("user@example.com", "password123");
var user = createUserWithId("user@example.com", "encoded-password");
when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(user));
when(passwordEncoder.matches("password123", "encoded-password")).thenReturn(true);
when(jwtService.generateToken(user)).thenReturn("login-jwt-token");
var response = authService.login(request, "192.168.1.1");
assertNotNull(response);
assertEquals("login-jwt-token", response.token());
assertEquals("user@example.com", response.email());
verify(auditService).log(
any(), eq("user@example.com"), eq(null),
eq(AuditAction.AUTH_LOGIN), eq(null),
eq(null), eq("192.168.1.1"),
eq("SUCCESS"), eq(null)
);
}
@Test
void login_rejectsInvalidPassword() {
var request = new LoginRequest("user@example.com", "wrong-password");
var user = createUserWithId("user@example.com", "encoded-password");
when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(user));
when(passwordEncoder.matches("wrong-password", "encoded-password")).thenReturn(false);
assertThrows(IllegalArgumentException.class,
() -> authService.login(request, "192.168.1.1"));
// Verify AUTH_LOGIN_FAILED audit was logged
verify(auditService).log(
any(), eq("user@example.com"), eq(null),
eq(AuditAction.AUTH_LOGIN_FAILED), eq(null),
eq(null), eq("192.168.1.1"),
eq("FAILURE"), eq(null)
);
}
@Test
void login_rejectsUnknownEmail() {
var request = new LoginRequest("unknown@example.com", "password123");
when(userRepository.findByEmail("unknown@example.com")).thenReturn(Optional.empty());
var exception = assertThrows(IllegalArgumentException.class,
() -> authService.login(request, "192.168.1.1"));
assertEquals("Invalid credentials", exception.getMessage());
verify(auditService, never()).log(any(), any(), any(), any(), any(), any(), any(), any(), any());
}
private UserEntity createUserWithId(String email, String password) {
var user = new UserEntity();
user.setEmail(email);
user.setName("Test User");
user.setPassword(password);
try {
var idField = UserEntity.class.getDeclaredField("id");
idField.setAccessible(true);
idField.set(user, UUID.randomUUID());
} catch (Exception e) {
throw new RuntimeException(e);
}
return user;
}
}

View File

@@ -13,6 +13,7 @@ import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -30,25 +31,12 @@ class LicenseControllerTest {
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
private String getAuthToken() throws Exception { private String createTenantAndGetId() throws Exception {
var registerRequest = new net.siegeln.cameleer.saas.auth.dto.RegisterRequest(
"license-test-" + System.nanoTime() + "@example.com", "Test User", "password123");
var result = mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isCreated())
.andReturn();
return objectMapper.readTree(result.getResponse().getContentAsString()).get("token").asText();
}
private String createTenantAndGetId(String token) throws Exception {
String slug = "license-tenant-" + System.nanoTime(); String slug = "license-tenant-" + System.nanoTime();
var request = new CreateTenantRequest("License Test Org", slug, "MID"); var request = new CreateTenantRequest("License Test Org", slug, "MID");
var result = mockMvc.perform(post("/api/tenants") var result = mockMvc.perform(post("/api/tenants")
.header("Authorization", "Bearer " + token) .with(jwt().jwt(j -> j.claim("sub", "test-user")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
@@ -59,11 +47,10 @@ class LicenseControllerTest {
@Test @Test
void generateLicense_returns201WithToken() throws Exception { void generateLicense_returns201WithToken() throws Exception {
String token = getAuthToken(); String tenantId = createTenantAndGetId();
String tenantId = createTenantAndGetId(token);
mockMvc.perform(post("/api/tenants/" + tenantId + "/license") mockMvc.perform(post("/api/tenants/" + tenantId + "/license")
.header("Authorization", "Bearer " + token)) .with(jwt().jwt(j -> j.claim("sub", "test-user"))))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andExpect(jsonPath("$.token").isNotEmpty()) .andExpect(jsonPath("$.token").isNotEmpty())
.andExpect(jsonPath("$.tier").value("MID")) .andExpect(jsonPath("$.tier").value("MID"))
@@ -72,26 +59,24 @@ class LicenseControllerTest {
@Test @Test
void getActiveLicense_returnsLicense() throws Exception { void getActiveLicense_returnsLicense() throws Exception {
String token = getAuthToken(); String tenantId = createTenantAndGetId();
String tenantId = createTenantAndGetId(token);
mockMvc.perform(post("/api/tenants/" + tenantId + "/license") mockMvc.perform(post("/api/tenants/" + tenantId + "/license")
.header("Authorization", "Bearer " + token)) .with(jwt().jwt(j -> j.claim("sub", "test-user"))))
.andExpect(status().isCreated()); .andExpect(status().isCreated());
mockMvc.perform(get("/api/tenants/" + tenantId + "/license") mockMvc.perform(get("/api/tenants/" + tenantId + "/license")
.header("Authorization", "Bearer " + token)) .with(jwt().jwt(j -> j.claim("sub", "test-user"))))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.tier").value("MID")); .andExpect(jsonPath("$.tier").value("MID"));
} }
@Test @Test
void getActiveLicense_returns404WhenNone() throws Exception { void getActiveLicense_returns404WhenNone() throws Exception {
String token = getAuthToken(); String tenantId = createTenantAndGetId();
String tenantId = createTenantAndGetId(token);
mockMvc.perform(get("/api/tenants/" + tenantId + "/license") mockMvc.perform(get("/api/tenants/" + tenantId + "/license")
.header("Authorization", "Bearer " + token)) .with(jwt().jwt(j -> j.claim("sub", "test-user"))))
.andExpect(status().isNotFound()); .andExpect(status().isNotFound());
} }
} }

View File

@@ -13,6 +13,7 @@ import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -30,26 +31,14 @@ class TenantControllerTest {
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
private String getAuthToken() throws Exception {
var registerRequest = new net.siegeln.cameleer.saas.auth.dto.RegisterRequest(
"tenant-test-" + System.nanoTime() + "@example.com", "Test User", "password123");
var result = mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isCreated())
.andReturn();
return objectMapper.readTree(result.getResponse().getContentAsString()).get("token").asText();
}
@Test @Test
void createTenant_returns201() throws Exception { void createTenant_returns201() throws Exception {
String token = getAuthToken();
var request = new CreateTenantRequest("Test Org", "test-org-" + System.nanoTime(), "LOW"); var request = new CreateTenantRequest("Test Org", "test-org-" + System.nanoTime(), "LOW");
mockMvc.perform(post("/api/tenants") mockMvc.perform(post("/api/tenants")
.header("Authorization", "Bearer " + token) .with(jwt().jwt(j -> j
.claim("sub", "test-user")
.claim("organization_id", "test-org")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
@@ -60,18 +49,17 @@ class TenantControllerTest {
@Test @Test
void createTenant_returns409ForDuplicateSlug() throws Exception { void createTenant_returns409ForDuplicateSlug() throws Exception {
String token = getAuthToken();
String slug = "duplicate-slug-" + System.nanoTime(); String slug = "duplicate-slug-" + System.nanoTime();
var request = new CreateTenantRequest("First", slug, null); var request = new CreateTenantRequest("First", slug, null);
mockMvc.perform(post("/api/tenants") mockMvc.perform(post("/api/tenants")
.header("Authorization", "Bearer " + token) .with(jwt().jwt(j -> j.claim("sub", "test-user")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()); .andExpect(status().isCreated());
mockMvc.perform(post("/api/tenants") mockMvc.perform(post("/api/tenants")
.header("Authorization", "Bearer " + token) .with(jwt().jwt(j -> j.claim("sub", "test-user")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict()); .andExpect(status().isConflict());
@@ -89,12 +77,11 @@ class TenantControllerTest {
@Test @Test
void getTenant_returnsTenantById() throws Exception { void getTenant_returnsTenantById() throws Exception {
String token = getAuthToken();
String slug = "get-test-" + System.nanoTime(); String slug = "get-test-" + System.nanoTime();
var request = new CreateTenantRequest("Get Test", slug, null); var request = new CreateTenantRequest("Get Test", slug, null);
var createResult = mockMvc.perform(post("/api/tenants") var createResult = mockMvc.perform(post("/api/tenants")
.header("Authorization", "Bearer " + token) .with(jwt().jwt(j -> j.claim("sub", "test-user")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
@@ -103,7 +90,7 @@ class TenantControllerTest {
String id = objectMapper.readTree(createResult.getResponse().getContentAsString()).get("id").asText(); String id = objectMapper.readTree(createResult.getResponse().getContentAsString()).get("id").asText();
mockMvc.perform(get("/api/tenants/" + id) mockMvc.perform(get("/api/tenants/" + id)
.header("Authorization", "Bearer " + token)) .with(jwt().jwt(j -> j.claim("sub", "test-user"))))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.slug").value(slug)); .andExpect(jsonPath("$.slug").value(slug));
} }