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:
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
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.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -30,25 +31,12 @@ class LicenseControllerTest {
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private String getAuthToken() 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 {
|
||||
private String createTenantAndGetId() throws Exception {
|
||||
String slug = "license-tenant-" + System.nanoTime();
|
||||
var request = new CreateTenantRequest("License Test Org", slug, "MID");
|
||||
|
||||
var result = mockMvc.perform(post("/api/tenants")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
@@ -59,11 +47,10 @@ class LicenseControllerTest {
|
||||
|
||||
@Test
|
||||
void generateLicense_returns201WithToken() throws Exception {
|
||||
String token = getAuthToken();
|
||||
String tenantId = createTenantAndGetId(token);
|
||||
String tenantId = createTenantAndGetId();
|
||||
|
||||
mockMvc.perform(post("/api/tenants/" + tenantId + "/license")
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.tier").value("MID"))
|
||||
@@ -72,26 +59,24 @@ class LicenseControllerTest {
|
||||
|
||||
@Test
|
||||
void getActiveLicense_returnsLicense() throws Exception {
|
||||
String token = getAuthToken();
|
||||
String tenantId = createTenantAndGetId(token);
|
||||
String tenantId = createTenantAndGetId();
|
||||
|
||||
mockMvc.perform(post("/api/tenants/" + tenantId + "/license")
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(get("/api/tenants/" + tenantId + "/license")
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.tier").value("MID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getActiveLicense_returns404WhenNone() throws Exception {
|
||||
String token = getAuthToken();
|
||||
String tenantId = createTenantAndGetId(token);
|
||||
String tenantId = createTenantAndGetId();
|
||||
|
||||
mockMvc.perform(get("/api/tenants/" + tenantId + "/license")
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
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.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -30,26 +31,14 @@ class TenantControllerTest {
|
||||
@Autowired
|
||||
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
|
||||
void createTenant_returns201() throws Exception {
|
||||
String token = getAuthToken();
|
||||
var request = new CreateTenantRequest("Test Org", "test-org-" + System.nanoTime(), "LOW");
|
||||
|
||||
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)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
@@ -60,18 +49,17 @@ class TenantControllerTest {
|
||||
|
||||
@Test
|
||||
void createTenant_returns409ForDuplicateSlug() throws Exception {
|
||||
String token = getAuthToken();
|
||||
String slug = "duplicate-slug-" + System.nanoTime();
|
||||
var request = new CreateTenantRequest("First", slug, null);
|
||||
|
||||
mockMvc.perform(post("/api/tenants")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(post("/api/tenants")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict());
|
||||
@@ -89,12 +77,11 @@ class TenantControllerTest {
|
||||
|
||||
@Test
|
||||
void getTenant_returnsTenantById() throws Exception {
|
||||
String token = getAuthToken();
|
||||
String slug = "get-test-" + System.nanoTime();
|
||||
var request = new CreateTenantRequest("Get Test", slug, null);
|
||||
|
||||
var createResult = mockMvc.perform(post("/api/tenants")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
@@ -103,7 +90,7 @@ class TenantControllerTest {
|
||||
String id = objectMapper.readTree(createResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
mockMvc.perform(get("/api/tenants/" + id)
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.slug").value(slug));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user