register, login, create and manage sites and pages

This commit is contained in:
Krrish Ghimire
2026-06-30 11:13:55 +05:45
commit 65f38d68c5
110 changed files with 28049 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
package com.indie.config;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import static org.junit.jupiter.api.Assertions.*;
class GlobalExceptionHandlerTest {
private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
@Test
void illegalArgumentReturns400() {
ProblemDetail detail = handler.handleBadRequest(new IllegalArgumentException("bad input"));
assertEquals(HttpStatus.BAD_REQUEST.value(), detail.getStatus());
assertEquals("bad input", detail.getDetail());
}
@Test
void validationReturns400WithJoinedMessages() {
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(new Object(), "obj");
bindingResult.addError(new FieldError("obj", "email", "Email is required"));
bindingResult.addError(new FieldError("obj", "password", "Password is required"));
MethodArgumentNotValidException ex = new MethodArgumentNotValidException(null, bindingResult);
ProblemDetail detail = handler.handleValidation(ex);
assertEquals(HttpStatus.BAD_REQUEST.value(), detail.getStatus());
assertTrue(detail.getDetail().contains("Email is required"));
assertTrue(detail.getDetail().contains("Password is required"));
}
@Test
void generalExceptionReturns500() {
ProblemDetail detail = handler.handleGeneral(new RuntimeException("oops"));
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), detail.getStatus());
assertEquals("An unexpected error occurred", detail.getDetail());
}
}

View File

@@ -0,0 +1,45 @@
package com.indie.config;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class UserPrincipalTest {
private final UUID id = UUID.randomUUID();
private final UserPrincipal principal = new UserPrincipal(id, "test@example.com", "Test User");
@Test
void getUsernameReturnsEmail() {
assertEquals("test@example.com", principal.getUsername());
}
@Test
void getPasswordReturnsNull() {
assertNull(principal.getPassword());
}
@Test
void authoritiesContainUserRole() {
var authorities = principal.getAuthorities();
assertEquals(1, authorities.size());
assertEquals("ROLE_USER", authorities.iterator().next().getAuthority());
}
@Test
void accountIsAlwaysEnabled() {
assertTrue(principal.isEnabled());
assertTrue(principal.isAccountNonExpired());
assertTrue(principal.isAccountNonLocked());
assertTrue(principal.isCredentialsNonExpired());
}
@Test
void recordComponents() {
assertEquals(id, principal.id());
assertEquals("test@example.com", principal.email());
assertEquals("Test User", principal.name());
}
}

View File

@@ -0,0 +1,141 @@
package com.indie.controller;
import com.indie.config.GlobalExceptionHandler;
import com.indie.dto.AuthResponse;
import com.indie.dto.AuthResponse.UserInfo;
import com.indie.dto.LoginRequest;
import com.indie.dto.RefreshTokenRequest;
import com.indie.dto.RegisterRequest;
import com.indie.model.AuthProvider;
import com.indie.service.AuthService;
import com.indie.support.TestSecurityConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(AuthController.class)
@Import({GlobalExceptionHandler.class, TestSecurityConfig.class})
class AuthControllerTest {
private final MockMvc mockMvc;
private final ObjectMapper objectMapper;
@MockBean
private AuthService authService;
AuthControllerTest(@Autowired MockMvc mockMvc, @Autowired ObjectMapper objectMapper) {
this.mockMvc = mockMvc;
this.objectMapper = objectMapper;
}
@Test
void registerReturns201() throws Exception {
AuthResponse response = AuthResponse.builder()
.accessToken("at-123")
.refreshToken("rt-456")
.tokenType("Bearer")
.user(new UserInfo(UUID.randomUUID(), "test@example.com", "Test", null, AuthProvider.LOCAL))
.build();
when(authService.register(any(RegisterRequest.class))).thenReturn(response);
RegisterRequest request = new RegisterRequest();
request.setEmail("test@example.com");
request.setPassword("password123");
request.setName("Test");
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accessToken").value("at-123"))
.andExpect(jsonPath("$.refreshToken").value("rt-456"))
.andExpect(jsonPath("$.tokenType").value("Bearer"))
.andExpect(jsonPath("$.user.email").value("test@example.com"));
}
@Test
void registerReturns400OnValidationError() throws Exception {
RegisterRequest request = new RegisterRequest();
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}
@Test
void loginReturns200() throws Exception {
AuthResponse response = AuthResponse.builder()
.accessToken("at-123")
.refreshToken("rt-456")
.tokenType("Bearer")
.user(new UserInfo(UUID.randomUUID(), "login@example.com", "Login User", null, AuthProvider.LOCAL))
.build();
when(authService.login(any(LoginRequest.class))).thenReturn(response);
LoginRequest request = new LoginRequest();
request.setEmail("login@example.com");
request.setPassword("password123");
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accessToken").value("at-123"))
.andExpect(jsonPath("$.user.email").value("login@example.com"));
}
@Test
void loginReturns400OnInvalidCredentials() throws Exception {
when(authService.login(any(LoginRequest.class)))
.thenThrow(new IllegalArgumentException("Invalid email or password"));
LoginRequest request = new LoginRequest();
request.setEmail("bad@example.com");
request.setPassword("wrong");
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.detail").value("Invalid email or password"));
}
@Test
void refreshReturns200() throws Exception {
AuthResponse response = AuthResponse.builder()
.accessToken("new-at")
.refreshToken("new-rt")
.tokenType("Bearer")
.user(new UserInfo(UUID.randomUUID(), "user@example.com", "User", null, AuthProvider.LOCAL))
.build();
when(authService.refreshToken(any(RefreshTokenRequest.class))).thenReturn(response);
RefreshTokenRequest request = new RefreshTokenRequest();
request.setRefreshToken("valid-refresh-token");
mockMvc.perform(post("/api/auth/refresh")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accessToken").value("new-at"));
}
@Test
void logoutReturns204() throws Exception {
mockMvc.perform(post("/api/auth/logout"))
.andExpect(status().isNoContent());
}
}

View File

@@ -0,0 +1,201 @@
package com.indie.controller;
import com.indie.config.GlobalExceptionHandler;
import com.indie.config.JwtAuthenticationToken;
import com.indie.config.UserPrincipal;
import com.indie.dto.PageRequest;
import com.indie.model.Page;
import com.indie.model.Site;
import com.indie.model.SiteStatus;
import com.indie.repository.PageRepository;
import com.indie.repository.SiteRepository;
import com.indie.support.TestSecurityConfig;
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.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(PageController.class)
@Import({GlobalExceptionHandler.class, TestSecurityConfig.class})
class PageControllerTest {
private final MockMvc mockMvc;
private final ObjectMapper objectMapper;
@MockBean
private PageRepository pageRepository;
@MockBean
private SiteRepository siteRepository;
private UUID userId;
PageControllerTest(@Autowired MockMvc mockMvc, @Autowired ObjectMapper objectMapper) {
this.mockMvc = mockMvc;
this.objectMapper = objectMapper;
}
private UUID siteId;
private UserPrincipal principal;
private JwtAuthenticationToken auth;
@BeforeEach
void setUp() {
userId = UUID.randomUUID();
siteId = UUID.randomUUID();
principal = new UserPrincipal(userId, "user@test.com", "Test User");
auth = new JwtAuthenticationToken(principal, "test-token");
}
@Test
void listPagesReturns200() throws Exception {
Site site = Site.builder().id(siteId).userId(userId).name("Site").build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
Page page = Page.builder()
.id(UUID.randomUUID())
.siteId(siteId)
.title("Home")
.slug("home")
.orderIndex(0)
.build();
when(pageRepository.findBySiteIdOrderByOrderIndexAsc(siteId))
.thenReturn(List.of(page));
mockMvc.perform(get("/api/sites/{siteId}/pages", siteId)
.with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Home"))
.andExpect(jsonPath("$[0].slug").value("home"));
}
@Test
void createPageReturns201() throws Exception {
Site site = Site.builder().id(siteId).userId(userId).name("Site").build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
when(pageRepository.existsBySiteIdAndSlug(siteId, "about")).thenReturn(false);
Page saved = Page.builder()
.id(UUID.randomUUID())
.siteId(siteId)
.title("About")
.slug("about")
.orderIndex(1)
.createdAt(LocalDateTime.now())
.build();
when(pageRepository.save(any(Page.class))).thenReturn(saved);
PageRequest request = new PageRequest();
request.setTitle("About");
request.setSlug("about");
request.setOrderIndex(1);
mockMvc.perform(post("/api/sites/{siteId}/pages", siteId)
.with(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value("About"))
.andExpect(jsonPath("$.slug").value("about"));
}
@Test
void createPageReturns400OnDuplicateSlug() throws Exception {
Site site = Site.builder().id(siteId).userId(userId).name("Site").build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
when(pageRepository.existsBySiteIdAndSlug(siteId, "home")).thenReturn(true);
PageRequest request = new PageRequest();
request.setTitle("Home");
request.setSlug("home");
request.setOrderIndex(0);
mockMvc.perform(post("/api/sites/{siteId}/pages", siteId)
.with(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.detail").value("A page with this slug already exists"));
}
@Test
void deletePageReturns204() throws Exception {
Site site = Site.builder().id(siteId).userId(userId).name("Site").build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
UUID pageId = UUID.randomUUID();
Page page = Page.builder()
.id(pageId)
.siteId(siteId)
.title("To Delete")
.slug("to-delete")
.orderIndex(0)
.build();
when(pageRepository.findById(pageId)).thenReturn(Optional.of(page));
mockMvc.perform(delete("/api/sites/{siteId}/pages/{pageId}", siteId, pageId)
.with(authentication(auth)))
.andExpect(status().isNoContent());
}
@Test
void updatePageReturns200() throws Exception {
Site site = Site.builder().id(siteId).userId(userId).name("Site").build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
UUID pageId = UUID.randomUUID();
Page existing = Page.builder()
.id(pageId)
.siteId(siteId)
.title("Old Title")
.slug("old-slug")
.orderIndex(0)
.build();
when(pageRepository.findById(pageId)).thenReturn(Optional.of(existing));
when(pageRepository.existsBySiteIdAndSlug(siteId, "new-slug")).thenReturn(false);
when(pageRepository.save(any(Page.class))).thenAnswer(invocation -> invocation.getArgument(0));
PageRequest request = new PageRequest();
request.setTitle("New Title");
request.setSlug("new-slug");
request.setOrderIndex(1);
mockMvc.perform(put("/api/sites/{siteId}/pages/{pageId}", siteId, pageId)
.with(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("New Title"))
.andExpect(jsonPath("$.slug").value("new-slug"));
}
@Test
void listPagesReturns400OnWrongSiteOwner() throws Exception {
Site site = Site.builder()
.id(siteId)
.userId(UUID.randomUUID())
.name("Not Mine")
.build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
mockMvc.perform(get("/api/sites/{siteId}/pages", siteId)
.with(authentication(auth)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.detail").value("Site not found"));
}
}

View File

@@ -0,0 +1,195 @@
package com.indie.controller;
import com.indie.config.GlobalExceptionHandler;
import com.indie.config.JwtAuthenticationToken;
import com.indie.config.UserPrincipal;
import com.indie.dto.SiteRequest;
import com.indie.model.Site;
import com.indie.model.SiteStatus;
import com.indie.repository.SiteRepository;
import com.indie.support.TestSecurityConfig;
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.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(SiteController.class)
@Import({GlobalExceptionHandler.class, TestSecurityConfig.class})
class SiteControllerTest {
private final MockMvc mockMvc;
private final ObjectMapper objectMapper;
@MockBean
private SiteRepository siteRepository;
private UUID userId;
SiteControllerTest(@Autowired MockMvc mockMvc, @Autowired ObjectMapper objectMapper) {
this.mockMvc = mockMvc;
this.objectMapper = objectMapper;
}
private UserPrincipal principal;
private JwtAuthenticationToken auth;
@BeforeEach
void setUp() {
userId = UUID.randomUUID();
principal = new UserPrincipal(userId, "user@test.com", "Test User");
auth = new JwtAuthenticationToken(principal, "test-token");
}
@Test
void listSitesReturns200() throws Exception {
Site site = Site.builder()
.id(UUID.randomUUID())
.userId(userId)
.name("My Site")
.status(SiteStatus.DRAFT)
.build();
when(siteRepository.findByUserIdOrderByCreatedAtDesc(userId))
.thenReturn(List.of(site));
mockMvc.perform(get("/api/sites")
.with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("My Site"))
.andExpect(jsonPath("$[0].status").value("DRAFT"));
}
@Test
void createSiteReturns201() throws Exception {
Site saved = Site.builder()
.id(UUID.randomUUID())
.userId(userId)
.name("New Site")
.description("Description")
.subdomain("new-site")
.status(SiteStatus.DRAFT)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
when(siteRepository.existsBySubdomain("new-site")).thenReturn(false);
when(siteRepository.save(any(Site.class))).thenReturn(saved);
SiteRequest request = new SiteRequest();
request.setName("New Site");
request.setDescription("Description");
request.setSubdomain("new-site");
mockMvc.perform(post("/api/sites")
.with(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("New Site"))
.andExpect(jsonPath("$.subdomain").value("new-site"));
}
@Test
void createSiteReturns400OnDuplicateSubdomain() throws Exception {
when(siteRepository.existsBySubdomain("taken")).thenReturn(true);
SiteRequest request = new SiteRequest();
request.setName("Site");
request.setSubdomain("taken");
mockMvc.perform(post("/api/sites")
.with(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}
@Test
void getSiteReturns200() throws Exception {
UUID siteId = UUID.randomUUID();
Site site = Site.builder()
.id(siteId)
.userId(userId)
.name("My Site")
.status(SiteStatus.PUBLISHED)
.build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
mockMvc.perform(get("/api/sites/{id}", siteId)
.with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("My Site"))
.andExpect(jsonPath("$.status").value("PUBLISHED"));
}
@Test
void getSiteReturns400OnWrongOwner() throws Exception {
UUID siteId = UUID.randomUUID();
Site site = Site.builder()
.id(siteId)
.userId(UUID.randomUUID())
.name("Not Mine")
.build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
mockMvc.perform(get("/api/sites/{id}", siteId)
.with(authentication(auth)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.detail").value("Site not found"));
}
@Test
void deleteSiteReturns204() throws Exception {
UUID siteId = UUID.randomUUID();
Site site = Site.builder()
.id(siteId)
.userId(userId)
.name("To Delete")
.build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
mockMvc.perform(delete("/api/sites/{id}", siteId)
.with(authentication(auth)))
.andExpect(status().isNoContent());
}
@Test
void updateSiteReturns200() throws Exception {
UUID siteId = UUID.randomUUID();
Site existing = Site.builder()
.id(siteId)
.userId(userId)
.name("Old Name")
.subdomain("old-sub")
.status(SiteStatus.DRAFT)
.build();
when(siteRepository.findById(siteId)).thenReturn(Optional.of(existing));
when(siteRepository.existsBySubdomain("new-sub")).thenReturn(false);
when(siteRepository.save(any(Site.class))).thenAnswer(invocation -> invocation.getArgument(0));
SiteRequest request = new SiteRequest();
request.setName("New Name");
request.setSubdomain("new-sub");
mockMvc.perform(put("/api/sites/{id}", siteId)
.with(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("New Name"))
.andExpect(jsonPath("$.subdomain").value("new-sub"));
}
}

View File

@@ -0,0 +1,57 @@
package com.indie.dto;
import com.indie.model.Page;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class PageResponseTest {
@Test
void fromMapsAllFields() {
UUID id = UUID.randomUUID();
UUID siteId = UUID.randomUUID();
LocalDateTime now = LocalDateTime.now();
Page page = Page.builder()
.id(id)
.siteId(siteId)
.title("About")
.slug("about")
.seoTitle("About Us")
.seoDescription("Learn about us")
.orderIndex(1)
.createdAt(now)
.build();
PageResponse response = PageResponse.from(page);
assertEquals(id, response.getId());
assertEquals(siteId, response.getSiteId());
assertEquals("About", response.getTitle());
assertEquals("about", response.getSlug());
assertEquals("About Us", response.getSeoTitle());
assertEquals("Learn about us", response.getSeoDescription());
assertEquals(1, response.getOrderIndex());
assertEquals(now, response.getCreatedAt());
}
@Test
void fromHandlesNullOptionalFields() {
Page page = Page.builder()
.id(UUID.randomUUID())
.siteId(UUID.randomUUID())
.title("Home")
.slug("home")
.orderIndex(0)
.build();
PageResponse response = PageResponse.from(page);
assertNull(response.getSeoTitle());
assertNull(response.getSeoDescription());
}
}

View File

@@ -0,0 +1,65 @@
package com.indie.dto;
import com.indie.model.Site;
import com.indie.model.SiteStatus;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class SiteResponseTest {
@Test
void fromMapsAllFields() {
UUID id = UUID.randomUUID();
UUID userId = UUID.randomUUID();
LocalDateTime now = LocalDateTime.now();
Map<String, Object> theme = Map.of("primaryColor", "#000");
Site site = Site.builder()
.id(id)
.userId(userId)
.name("My Site")
.description("A test site")
.subdomain("mysite")
.themeConfig(theme)
.status(SiteStatus.PUBLISHED)
.publishedAt(now)
.createdAt(now)
.updatedAt(now)
.build();
SiteResponse response = SiteResponse.from(site);
assertEquals(id, response.getId());
assertEquals(userId, response.getUserId());
assertEquals("My Site", response.getName());
assertEquals("A test site", response.getDescription());
assertEquals("mysite", response.getSubdomain());
assertEquals(theme, response.getThemeConfig());
assertEquals(SiteStatus.PUBLISHED, response.getStatus());
assertEquals(now, response.getPublishedAt());
assertEquals(now, response.getCreatedAt());
assertEquals(now, response.getUpdatedAt());
}
@Test
void fromHandlesNullOptionalFields() {
Site site = Site.builder()
.id(UUID.randomUUID())
.userId(UUID.randomUUID())
.name("Site")
.status(SiteStatus.DRAFT)
.build();
SiteResponse response = SiteResponse.from(site);
assertNull(response.getDescription());
assertNull(response.getSubdomain());
assertNull(response.getThemeConfig());
assertNull(response.getPublishedAt());
}
}

View File

@@ -0,0 +1,39 @@
package com.indie.model;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class ComponentTest {
@Test
void prePersistSetsId() {
Component component = new Component();
component.setPageId(UUID.randomUUID());
component.setType(ComponentType.HEADING);
component.setOrderIndex(0);
assertNull(component.getId());
component.onCreate();
assertNotNull(component.getId());
}
@Test
void prePersistDoesNotOverrideExistingId() {
UUID existingId = UUID.randomUUID();
Component component = Component.builder()
.id(existingId)
.pageId(UUID.randomUUID())
.type(ComponentType.TEXT)
.orderIndex(0)
.build();
component.onCreate();
assertEquals(existingId, component.getId());
}
}

View File

@@ -0,0 +1,57 @@
package com.indie.model;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class PageTest {
@Test
void prePersistSetsIdAndCreatedAt() {
Page page = new Page();
page.setSiteId(UUID.randomUUID());
page.setTitle("Home");
page.setSlug("home");
page.setOrderIndex(0);
assertNull(page.getId());
assertNull(page.getCreatedAt());
page.onCreate();
assertNotNull(page.getId());
assertNotNull(page.getCreatedAt());
}
@Test
void prePersistDoesNotOverrideExistingId() {
UUID existingId = UUID.randomUUID();
Page page = Page.builder()
.id(existingId)
.siteId(UUID.randomUUID())
.title("Home")
.slug("home")
.orderIndex(0)
.build();
page.onCreate();
assertEquals(existingId, page.getId());
}
@Test
void noUpdatedAtField() {
Page page = Page.builder()
.siteId(UUID.randomUUID())
.title("Home")
.slug("home")
.orderIndex(0)
.build();
page.onCreate();
assertNotNull(page.getCreatedAt());
}
}

View File

@@ -0,0 +1,68 @@
package com.indie.model;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class SiteTest {
@Test
void prePersistSetsIdCreatedAtUpdatedAtAndDefaultStatus() {
Site site = new Site();
site.setUserId(UUID.randomUUID());
site.setName("Test Site");
assertNull(site.getId());
assertNull(site.getCreatedAt());
assertNull(site.getUpdatedAt());
site.onCreate();
assertNotNull(site.getId());
assertNotNull(site.getCreatedAt());
assertNotNull(site.getUpdatedAt());
assertEquals(SiteStatus.DRAFT, site.getStatus());
}
@Test
void prePersistDoesNotOverrideExistingId() {
UUID existingId = UUID.randomUUID();
Site site = Site.builder()
.id(existingId)
.userId(UUID.randomUUID())
.name("Test")
.build();
site.onCreate();
assertEquals(existingId, site.getId());
}
@Test
void preUpdateSetsUpdatedAt() {
Site site = Site.builder()
.userId(UUID.randomUUID())
.name("Test")
.build();
site.onCreate();
LocalDateTime original = site.getUpdatedAt();
site.onUpdate();
assertTrue(site.getUpdatedAt().isAfter(original));
}
@Test
void builderSetsDefaultStatus() {
Site site = Site.builder()
.userId(UUID.randomUUID())
.name("Test")
.build();
assertEquals(SiteStatus.DRAFT, site.getStatus());
}
}

View File

@@ -0,0 +1,71 @@
package com.indie.model;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class UserTest {
@Test
void prePersistSetsIdCreatedAtAndUpdatedAt() {
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
user.setProvider(AuthProvider.LOCAL);
assertNull(user.getId());
assertNull(user.getCreatedAt());
assertNull(user.getUpdatedAt());
user.onCreate();
assertNotNull(user.getId());
assertNotNull(user.getCreatedAt());
assertNotNull(user.getUpdatedAt());
}
@Test
void prePersistDoesNotOverrideExistingId() {
User user = User.builder()
.id(UUID.randomUUID())
.email("test@example.com")
.name("Test")
.provider(AuthProvider.LOCAL)
.build();
UUID existingId = user.getId();
user.onCreate();
assertEquals(existingId, user.getId());
}
@Test
void preUpdateSetsUpdatedAt() {
User user = User.builder()
.email("test@example.com")
.name("Test")
.provider(AuthProvider.LOCAL)
.build();
user.onCreate();
LocalDateTime original = user.getUpdatedAt();
user.onUpdate();
assertTrue(user.getUpdatedAt().isAfter(original));
}
@Test
void builderDefaults() {
User user = User.builder()
.email("test@example.com")
.name("Test")
.provider(AuthProvider.LOCAL)
.build();
assertFalse(user.isEmailVerified());
}
}

View File

@@ -0,0 +1,92 @@
package com.indie.repository;
import com.indie.model.Component;
import com.indie.model.ComponentType;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class ComponentRepositoryTest {
private final ComponentRepository componentRepository;
ComponentRepositoryTest(@Autowired ComponentRepository componentRepository) {
this.componentRepository = componentRepository;
}
@Test
void saveAndFindById() {
Component component = Component.builder()
.pageId(UUID.randomUUID())
.type(ComponentType.HEADING)
.orderIndex(0)
.build();
Component saved = componentRepository.save(component);
assertNotNull(saved.getId());
var found = componentRepository.findById(saved.getId());
assertTrue(found.isPresent());
assertEquals(ComponentType.HEADING, found.get().getType());
}
@Test
void findByPageIdOrderByOrderIndexAsc() {
UUID pageId = UUID.randomUUID();
Component c1 = Component.builder().pageId(pageId).type(ComponentType.TEXT).orderIndex(1).build();
Component c2 = Component.builder().pageId(pageId).type(ComponentType.HEADING).orderIndex(0).build();
componentRepository.save(c1);
componentRepository.save(c2);
List<Component> components = componentRepository.findByPageIdOrderByOrderIndexAsc(pageId);
assertEquals(2, components.size());
assertEquals(ComponentType.HEADING, components.get(0).getType());
assertEquals(ComponentType.TEXT, components.get(1).getType());
}
@Test
void findByPageIdReturnsEmptyForUnknownPage() {
List<Component> components = componentRepository.findByPageIdOrderByOrderIndexAsc(UUID.randomUUID());
assertTrue(components.isEmpty());
}
@Test
void deleteByPageId() {
UUID pageId = UUID.randomUUID();
Component c1 = Component.builder().pageId(pageId).type(ComponentType.BUTTON).orderIndex(0).build();
Component c2 = Component.builder().pageId(pageId).type(ComponentType.TEXT).orderIndex(1).build();
componentRepository.save(c1);
componentRepository.save(c2);
componentRepository.deleteByPageId(pageId);
List<Component> remaining = componentRepository.findByPageIdOrderByOrderIndexAsc(pageId);
assertTrue(remaining.isEmpty());
}
@Test
void deleteByPageIdOnlyDeletesForGivenPage() {
UUID pageId1 = UUID.randomUUID();
UUID pageId2 = UUID.randomUUID();
Component c1 = Component.builder().pageId(pageId1).type(ComponentType.GALLERY).orderIndex(0).build();
Component c2 = Component.builder().pageId(pageId2).type(ComponentType.IMAGE).orderIndex(0).build();
componentRepository.save(c1);
componentRepository.save(c2);
componentRepository.deleteByPageId(pageId1);
assertEquals(1, componentRepository.findByPageIdOrderByOrderIndexAsc(pageId2).size());
assertEquals(0, componentRepository.findByPageIdOrderByOrderIndexAsc(pageId1).size());
}
}

View File

@@ -0,0 +1,101 @@
package com.indie.repository;
import com.indie.model.Page;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class PageRepositoryTest {
private final PageRepository pageRepository;
PageRepositoryTest(@Autowired PageRepository pageRepository) {
this.pageRepository = pageRepository;
}
@Test
void saveAndFindById() {
Page page = Page.builder()
.siteId(UUID.randomUUID())
.title("Home")
.slug("home")
.orderIndex(0)
.build();
Page saved = pageRepository.save(page);
assertNotNull(saved.getId());
Optional<Page> found = pageRepository.findById(saved.getId());
assertTrue(found.isPresent());
assertEquals("Home", found.get().getTitle());
}
@Test
void findBySiteIdOrderByOrderIndexAsc() {
UUID siteId = UUID.randomUUID();
Page page1 = Page.builder().siteId(siteId).title("About").slug("about").orderIndex(1).build();
Page page2 = Page.builder().siteId(siteId).title("Home").slug("home").orderIndex(0).build();
pageRepository.save(page1);
pageRepository.save(page2);
List<Page> pages = pageRepository.findBySiteIdOrderByOrderIndexAsc(siteId);
assertEquals(2, pages.size());
assertEquals("home", pages.get(0).getSlug());
assertEquals("about", pages.get(1).getSlug());
}
@Test
void findBySiteIdReturnsEmptyForUnknownSite() {
List<Page> pages = pageRepository.findBySiteIdOrderByOrderIndexAsc(UUID.randomUUID());
assertTrue(pages.isEmpty());
}
@Test
void findBySiteIdAndSlug() {
UUID siteId = UUID.randomUUID();
Page page = Page.builder()
.siteId(siteId)
.title("Contact")
.slug("contact")
.orderIndex(2)
.build();
pageRepository.save(page);
Optional<Page> found = pageRepository.findBySiteIdAndSlug(siteId, "contact");
assertTrue(found.isPresent());
assertEquals("Contact", found.get().getTitle());
}
@Test
void findBySiteIdAndSlugReturnsEmptyWhenNotMatching() {
Optional<Page> found = pageRepository.findBySiteIdAndSlug(UUID.randomUUID(), "nonexistent");
assertTrue(found.isEmpty());
}
@Test
void existsBySiteIdAndSlug() {
UUID siteId = UUID.randomUUID();
Page page = Page.builder()
.siteId(siteId)
.title("Blog")
.slug("blog")
.orderIndex(0)
.build();
pageRepository.save(page);
assertTrue(pageRepository.existsBySiteIdAndSlug(siteId, "blog"));
assertFalse(pageRepository.existsBySiteIdAndSlug(siteId, "nonexistent"));
}
}

View File

@@ -0,0 +1,89 @@
package com.indie.repository;
import com.indie.model.Site;
import com.indie.model.SiteStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class SiteRepositoryTest {
private final SiteRepository siteRepository;
SiteRepositoryTest(@Autowired SiteRepository siteRepository) {
this.siteRepository = siteRepository;
}
@Test
void saveAndFindById() {
Site site = Site.builder()
.userId(UUID.randomUUID())
.name("Test Site")
.build();
Site saved = siteRepository.save(site);
assertNotNull(saved.getId());
assertEquals(SiteStatus.DRAFT, saved.getStatus());
Optional<Site> found = siteRepository.findById(saved.getId());
assertTrue(found.isPresent());
assertEquals("Test Site", found.get().getName());
}
@Test
void findByUserIdOrderByCreatedAtDesc() {
UUID userId = UUID.randomUUID();
Site site1 = Site.builder().userId(userId).name("Site 1").build();
Site site2 = Site.builder().userId(userId).name("Site 2").build();
siteRepository.save(site1);
siteRepository.save(site2);
List<Site> sites = siteRepository.findByUserIdOrderByCreatedAtDesc(userId);
assertEquals(2, sites.size());
assertTrue(sites.get(0).getCreatedAt().isAfter(sites.get(1).getCreatedAt())
|| sites.get(0).getCreatedAt().equals(sites.get(1).getCreatedAt()));
}
@Test
void findByUserIdReturnsEmptyForUnknownUser() {
List<Site> sites = siteRepository.findByUserIdOrderByCreatedAtDesc(UUID.randomUUID());
assertTrue(sites.isEmpty());
}
@Test
void findBySubdomain() {
Site site = Site.builder()
.userId(UUID.randomUUID())
.name("Site")
.subdomain("mysubdomain")
.build();
siteRepository.save(site);
Optional<Site> found = siteRepository.findBySubdomain("mysubdomain");
assertTrue(found.isPresent());
assertEquals("Site", found.get().getName());
}
@Test
void existsBySubdomain() {
Site site = Site.builder()
.userId(UUID.randomUUID())
.name("Site")
.subdomain("exists-sub")
.build();
siteRepository.save(site);
assertTrue(siteRepository.existsBySubdomain("exists-sub"));
assertFalse(siteRepository.existsBySubdomain("no-sub"));
}
}

View File

@@ -0,0 +1,89 @@
package com.indie.repository;
import com.indie.model.AuthProvider;
import com.indie.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class UserRepositoryTest {
private final UserRepository userRepository;
UserRepositoryTest(@Autowired UserRepository userRepository) {
this.userRepository = userRepository;
}
@Test
void saveAndFindById() {
User user = User.builder()
.email("test@example.com")
.name("Test User")
.provider(AuthProvider.LOCAL)
.build();
User saved = userRepository.save(user);
assertNotNull(saved.getId());
Optional<User> found = userRepository.findById(saved.getId());
assertTrue(found.isPresent());
assertEquals("test@example.com", found.get().getEmail());
}
@Test
void findByEmail() {
User user = User.builder()
.email("find@example.com")
.name("Find Me")
.provider(AuthProvider.LOCAL)
.build();
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("find@example.com");
assertTrue(found.isPresent());
assertEquals("Find Me", found.get().getName());
}
@Test
void findByEmailReturnsEmptyWhenNotFound() {
Optional<User> found = userRepository.findByEmail("doesnotexist@example.com");
assertTrue(found.isEmpty());
}
@Test
void findByProviderAndProviderId() {
User user = User.builder()
.email("oauth@example.com")
.name("OAuth User")
.provider(AuthProvider.GOOGLE)
.providerId("google-123")
.build();
userRepository.save(user);
Optional<User> found = userRepository.findByProviderAndProviderId(AuthProvider.GOOGLE, "google-123");
assertTrue(found.isPresent());
assertEquals("oauth@example.com", found.get().getEmail());
}
@Test
void existsByEmail() {
User user = User.builder()
.email("exists@example.com")
.name("Exists")
.provider(AuthProvider.LOCAL)
.build();
userRepository.save(user);
assertTrue(userRepository.existsByEmail("exists@example.com"));
assertFalse(userRepository.existsByEmail("noone@example.com"));
}
}

View File

@@ -0,0 +1,190 @@
package com.indie.service;
import com.indie.dto.AuthResponse;
import com.indie.dto.LoginRequest;
import com.indie.dto.RefreshTokenRequest;
import com.indie.dto.RegisterRequest;
import com.indie.model.AuthProvider;
import com.indie.model.User;
import com.indie.support.FakeJwtConfig;
import com.indie.support.FakeUserRepository;
import com.indie.support.StubPasswordEncoder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AuthServiceTest {
private FakeUserRepository userRepository;
private AuthService authService;
@BeforeEach
void setUp() {
userRepository = new FakeUserRepository();
authService = new AuthService(userRepository, new StubPasswordEncoder(), new FakeJwtConfig());
}
@Test
void registerCreatesUserAndReturnsTokens() {
RegisterRequest request = new RegisterRequest();
request.setEmail("new@example.com");
request.setPassword("password123");
request.setName("New User");
AuthResponse response = authService.register(request);
assertNotNull(response.getAccessToken());
assertNotNull(response.getRefreshToken());
assertEquals("Bearer", response.getTokenType());
assertEquals("new@example.com", response.getUser().getEmail());
assertEquals("New User", response.getUser().getName());
assertEquals(AuthProvider.LOCAL, response.getUser().getProvider());
}
@Test
void registerThrowsOnDuplicateEmail() {
RegisterRequest request = new RegisterRequest();
request.setEmail("dup@example.com");
request.setPassword("password123");
request.setName("User");
authService.register(request);
RegisterRequest duplicate = new RegisterRequest();
duplicate.setEmail("dup@example.com");
duplicate.setPassword("password456");
duplicate.setName("Other");
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> authService.register(duplicate));
assertEquals("Email already registered", ex.getMessage());
}
@Test
void registerNormalizesEmailCase() {
RegisterRequest request = new RegisterRequest();
request.setEmail("UpperCase@Example.com");
request.setPassword("password123");
request.setName("User");
AuthResponse response = authService.register(request);
assertEquals("uppercase@example.com", response.getUser().getEmail());
}
@Test
void loginWithValidCredentialsReturnsTokens() {
RegisterRequest reg = new RegisterRequest();
reg.setEmail("login@example.com");
reg.setPassword("password123");
reg.setName("Login User");
authService.register(reg);
LoginRequest login = new LoginRequest();
login.setEmail("login@example.com");
login.setPassword("password123");
AuthResponse response = authService.login(login);
assertNotNull(response.getAccessToken());
assertNotNull(response.getRefreshToken());
assertEquals("login@example.com", response.getUser().getEmail());
}
@Test
void loginThrowsOnWrongPassword() {
RegisterRequest reg = new RegisterRequest();
reg.setEmail("wrongpw@example.com");
reg.setPassword("correctpw");
reg.setName("User");
authService.register(reg);
LoginRequest login = new LoginRequest();
login.setEmail("wrongpw@example.com");
login.setPassword("wrongpw");
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> authService.login(login));
assertEquals("Invalid email or password", ex.getMessage());
}
@Test
void loginThrowsOnNonExistentEmail() {
LoginRequest login = new LoginRequest();
login.setEmail("nonexistent@example.com");
login.setPassword("password123");
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> authService.login(login));
assertEquals("Invalid email or password", ex.getMessage());
}
@Test
void loginThrowsOnOAuthUserTryingPasswordLogin() {
User oauthUser = User.builder()
.email("oauth@example.com")
.name("OAuth User")
.provider(AuthProvider.GOOGLE)
.providerId("google-123")
.build();
userRepository.save(oauthUser);
LoginRequest login = new LoginRequest();
login.setEmail("oauth@example.com");
login.setPassword("anypassword");
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> authService.login(login));
assertEquals("Please sign in with google", ex.getMessage());
}
@Test
void refreshTokenReturnsNewTokens() {
RegisterRequest reg = new RegisterRequest();
reg.setEmail("refresh@example.com");
reg.setPassword("password123");
reg.setName("Refresh User");
AuthResponse initial = authService.register(reg);
RefreshTokenRequest refreshRequest = new RefreshTokenRequest();
refreshRequest.setRefreshToken(initial.getRefreshToken());
AuthResponse response = authService.refreshToken(refreshRequest);
assertNotNull(response.getAccessToken());
assertNotNull(response.getRefreshToken());
assertEquals("refresh@example.com", response.getUser().getEmail());
}
@Test
void refreshTokenThrowsOnInvalidToken() {
RefreshTokenRequest request = new RefreshTokenRequest();
request.setRefreshToken("invalid-token-here");
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> authService.refreshToken(request));
assertEquals("Invalid or expired refresh token", ex.getMessage());
}
@Test
void refreshTokenThrowsWhenUserDeleted() {
User user = User.builder()
.email("deleted@example.com")
.name("Deleted")
.provider(AuthProvider.LOCAL)
.build();
userRepository.save(user);
FakeJwtConfig jwtConfig = new FakeJwtConfig();
String token = jwtConfig.generateRefreshToken(user);
userRepository.deleteById(user.getId());
RefreshTokenRequest request = new RefreshTokenRequest();
request.setRefreshToken(token);
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> authService.refreshToken(request));
assertEquals("Invalid or expired refresh token", ex.getMessage());
}
}

View File

@@ -0,0 +1,129 @@
package com.indie.service;
import com.indie.model.AuthProvider;
import com.indie.model.User;
import com.indie.support.FakeUserRepository;
import com.indie.support.TestOAuth2User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class OAuth2UserServiceTest {
private FakeUserRepository userRepository;
private OAuth2UserService oAuth2UserService;
@BeforeEach
void setUp() {
userRepository = new FakeUserRepository();
oAuth2UserService = new OAuth2UserService(userRepository);
}
@Test
void getProviderIdForGoogleReturnsSub() {
TestOAuth2User oAuth2User = new TestOAuth2User(Map.of("sub", "google-123"));
assertEquals("google-123", oAuth2UserService.getProviderId(oAuth2User, AuthProvider.GOOGLE));
}
@Test
void getProviderIdForGitHubReturnsId() {
TestOAuth2User oAuth2User = new TestOAuth2User(Map.of("id", 456));
assertEquals("456", oAuth2UserService.getProviderId(oAuth2User, AuthProvider.GITHUB));
}
@Test
void getAvatarUrlForGoogleReturnsPicture() {
TestOAuth2User oAuth2User = new TestOAuth2User(Map.of("picture", "https://example.com/avatar.jpg"));
assertEquals("https://example.com/avatar.jpg",
oAuth2UserService.getAvatarUrl(oAuth2User, AuthProvider.GOOGLE));
}
@Test
void getAvatarUrlForGitHubReturnsAvatarUrl() {
TestOAuth2User oAuth2User = new TestOAuth2User(Map.of("avatar_url", "https://github.com/avatar.png"));
assertEquals("https://github.com/avatar.png",
oAuth2UserService.getAvatarUrl(oAuth2User, AuthProvider.GITHUB));
}
@Test
void getAvatarUrlForOpenIdReturnsNull() {
TestOAuth2User oAuth2User = new TestOAuth2User(Map.of());
assertNull(oAuth2UserService.getAvatarUrl(oAuth2User, AuthProvider.OPENID));
}
@Test
void getAttributeReturnsStringValue() {
TestOAuth2User oAuth2User = new TestOAuth2User(Map.of("email", "test@example.com"));
assertEquals("test@example.com", oAuth2UserService.getAttribute(oAuth2User, "email"));
}
@Test
void getAttributeReturnsNullWhenMissing() {
TestOAuth2User oAuth2User = new TestOAuth2User(Map.of());
assertNull(oAuth2UserService.getAttribute(oAuth2User, "missing"));
}
@Test
void findOrCreateUserCreatesNewUser() {
User user = oAuth2UserService.findOrCreateUser(
AuthProvider.GOOGLE, "google-789",
"new@example.com", "New User", "https://example.com/avatar.jpg");
assertNotNull(user.getId());
assertEquals("new@example.com", user.getEmail());
assertEquals("New User", user.getName());
assertEquals(AuthProvider.GOOGLE, user.getProvider());
assertEquals("google-789", user.getProviderId());
assertTrue(user.isEmailVerified());
}
@Test
void findOrCreateUserUpdatesExistingOAuthUser() {
User existing = User.builder()
.email("existing@example.com")
.name("Old Name")
.provider(AuthProvider.GOOGLE)
.providerId("google-456")
.build();
userRepository.save(existing);
User updated = oAuth2UserService.findOrCreateUser(
AuthProvider.GOOGLE, "google-456",
"existing@example.com", "Updated Name", "https://example.com/new-avatar.jpg");
assertEquals("Updated Name", updated.getName());
assertEquals("https://example.com/new-avatar.jpg", updated.getAvatarUrl());
}
@Test
void findOrCreateUserLinksLocalUserToOAuth() {
User localUser = User.builder()
.email("link@example.com")
.name("Link User")
.provider(AuthProvider.LOCAL)
.password("hashedpw")
.build();
userRepository.save(localUser);
User linked = oAuth2UserService.findOrCreateUser(
AuthProvider.GITHUB, "github-789",
"link@example.com", "Link User", "https://github.com/avatar.png");
assertEquals(AuthProvider.GITHUB, linked.getProvider());
assertEquals("github-789", linked.getProviderId());
assertTrue(linked.isEmailVerified());
}
@Test
void findOrCreateUserGeneratesEmailWhenMissing() {
User user = oAuth2UserService.findOrCreateUser(
AuthProvider.GITHUB, "github-nomail",
null, "No Email", null);
assertTrue(user.getEmail().contains("github-nomail"));
assertTrue(user.getEmail().endsWith("@github.indie"));
}
}

View File

@@ -0,0 +1,40 @@
package com.indie.support;
import com.indie.model.Component;
import com.indie.repository.ComponentRepository;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
public class FakeComponentRepository extends InMemoryRepository<Component, UUID> implements ComponentRepository {
@Override
protected UUID getId(Component entity) {
return entity.getId();
}
@Override
protected Component setId(Component entity, UUID id) {
entity.setId(id);
return entity;
}
@Override
protected UUID generateId() {
return UUID.randomUUID();
}
@Override
public List<Component> findByPageIdOrderByOrderIndexAsc(UUID pageId) {
return store.values().stream()
.filter(c -> c.getPageId().equals(pageId))
.sorted(Comparator.comparingInt(Component::getOrderIndex))
.toList();
}
@Override
public void deleteByPageId(UUID pageId) {
store.values().removeIf(c -> c.getPageId().equals(pageId));
}
}

View File

@@ -0,0 +1,49 @@
package com.indie.support;
import com.indie.config.TokenService;
import com.indie.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import java.util.Date;
import static io.jsonwebtoken.Jwts.SIG.HS256;
public class FakeJwtConfig implements TokenService {
private static final long ACCESS_EXPIRATION = 900_000;
private static final long REFRESH_EXPIRATION = 604_800_000;
private static final SecretKey SECRET_KEY = HS256.key().build();
@Override
public String generateAccessToken(User user) {
return generateToken(user, ACCESS_EXPIRATION);
}
@Override
public String generateRefreshToken(User user) {
return generateToken(user, REFRESH_EXPIRATION);
}
private String generateToken(User user, long expiration) {
Date now = new Date();
return Jwts.builder()
.subject(user.getId().toString())
.claim("email", user.getEmail())
.claim("name", user.getName())
.issuedAt(now)
.expiration(new Date(now.getTime() + expiration))
.signWith(SECRET_KEY)
.compact();
}
@Override
public Claims validateToken(String token) {
return Jwts.parser()
.verifyWith(SECRET_KEY)
.build()
.parseSignedClaims(token)
.getPayload();
}
}

View File

@@ -0,0 +1,49 @@
package com.indie.support;
import com.indie.model.Page;
import com.indie.repository.PageRepository;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class FakePageRepository extends InMemoryRepository<Page, UUID> implements PageRepository {
@Override
protected UUID getId(Page entity) {
return entity.getId();
}
@Override
protected Page setId(Page entity, UUID id) {
entity.setId(id);
return entity;
}
@Override
protected UUID generateId() {
return UUID.randomUUID();
}
@Override
public List<Page> findBySiteIdOrderByOrderIndexAsc(UUID siteId) {
return store.values().stream()
.filter(p -> p.getSiteId().equals(siteId))
.sorted(Comparator.comparingInt(Page::getOrderIndex))
.toList();
}
@Override
public Optional<Page> findBySiteIdAndSlug(UUID siteId, String slug) {
return store.values().stream()
.filter(p -> p.getSiteId().equals(siteId) && p.getSlug().equals(slug))
.findFirst();
}
@Override
public boolean existsBySiteIdAndSlug(UUID siteId, String slug) {
return store.values().stream()
.anyMatch(p -> p.getSiteId().equals(siteId) && p.getSlug().equals(slug));
}
}

View File

@@ -0,0 +1,49 @@
package com.indie.support;
import com.indie.model.Site;
import com.indie.repository.SiteRepository;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class FakeSiteRepository extends InMemoryRepository<Site, UUID> implements SiteRepository {
@Override
protected UUID getId(Site entity) {
return entity.getId();
}
@Override
protected Site setId(Site entity, UUID id) {
entity.setId(id);
return entity;
}
@Override
protected UUID generateId() {
return UUID.randomUUID();
}
@Override
public List<Site> findByUserIdOrderByCreatedAtDesc(UUID userId) {
return store.values().stream()
.filter(s -> s.getUserId().equals(userId))
.sorted(Comparator.comparing(Site::getCreatedAt).reversed())
.toList();
}
@Override
public Optional<Site> findBySubdomain(String subdomain) {
return store.values().stream()
.filter(s -> subdomain.equals(s.getSubdomain()))
.findFirst();
}
@Override
public boolean existsBySubdomain(String subdomain) {
return store.values().stream()
.anyMatch(s -> subdomain.equals(s.getSubdomain()));
}
}

View File

@@ -0,0 +1,47 @@
package com.indie.support;
import com.indie.model.AuthProvider;
import com.indie.model.User;
import com.indie.repository.UserRepository;
import java.util.Optional;
import java.util.UUID;
public class FakeUserRepository extends InMemoryRepository<User, UUID> implements UserRepository {
@Override
protected UUID getId(User entity) {
return entity.getId();
}
@Override
protected User setId(User entity, UUID id) {
entity.setId(id);
return entity;
}
@Override
protected UUID generateId() {
return UUID.randomUUID();
}
@Override
public Optional<User> findByEmail(String email) {
return store.values().stream()
.filter(u -> u.getEmail().equals(email))
.findFirst();
}
@Override
public Optional<User> findByProviderAndProviderId(AuthProvider provider, String providerId) {
return store.values().stream()
.filter(u -> u.getProvider() == provider && providerId.equals(u.getProviderId()))
.findFirst();
}
@Override
public boolean existsByEmail(String email) {
return store.values().stream()
.anyMatch(u -> u.getEmail().equals(email));
}
}

View File

@@ -0,0 +1,209 @@
package com.indie.support;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.FluentQuery;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
public abstract class InMemoryRepository<T, ID> implements JpaRepository<T, ID> {
protected final Map<ID, T> store = new ConcurrentHashMap<>();
protected abstract ID getId(T entity);
protected abstract T setId(T entity, ID id);
protected abstract ID generateId();
@Override
@SuppressWarnings("unchecked")
public <S extends T> S save(S entity) {
ID id = getId(entity);
if (id == null) {
id = generateId();
setId(entity, id);
}
store.put(id, entity);
return entity;
}
@Override
public <S extends T> List<S> saveAll(Iterable<S> entities) {
List<S> saved = new ArrayList<>();
for (S entity : entities) {
saved.add(save(entity));
}
return saved;
}
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(store.get(id));
}
@Override
public boolean existsById(ID id) {
return store.containsKey(id);
}
@Override
public List<T> findAll() {
return new ArrayList<>(store.values());
}
@Override
public List<T> findAllById(Iterable<ID> ids) {
List<T> result = new ArrayList<>();
for (ID id : ids) {
T entity = store.get(id);
if (entity != null) {
result.add(entity);
}
}
return result;
}
@Override
public long count() {
return store.size();
}
@Override
public void deleteById(ID id) {
store.remove(id);
}
@Override
public void delete(T entity) {
ID id = getId(entity);
if (id != null) {
store.remove(id);
}
}
@Override
public void deleteAll(Iterable<? extends T> entities) {
for (T entity : entities) {
delete(entity);
}
}
@Override
public void deleteAllById(Iterable<? extends ID> ids) {
for (ID id : ids) {
store.remove(id);
}
}
@Override
public void deleteAll() {
store.clear();
}
@Override
public void deleteAllInBatch() {
store.clear();
}
@Override
public void deleteAllInBatch(Iterable<T> entities) {
for (T entity : entities) {
delete(entity);
}
}
@Override
public void deleteAllByIdInBatch(Iterable<ID> ids) {
for (ID id : ids) {
store.remove(id);
}
}
@Override
public void flush() {
}
@Override
public <S extends T> S saveAndFlush(S entity) {
return save(entity);
}
@Override
public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {
return saveAll(entities);
}
@Override
public List<T> findAll(Sort sort) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public Page<T> findAll(Pageable pageable) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public T getReferenceById(ID id) {
return store.get(id);
}
@Override
public T getById(ID id) {
return store.get(id);
}
@Override
@Deprecated
public T getOne(ID id) {
return store.get(id);
}
@Override
public <S extends T> Optional<S> findOne(Example<S> example) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public <S extends T> List<S> findAll(Example<S> example) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public <S extends T> long count(Example<S> example) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public <S extends T> boolean exists(Example<S> example) {
throw new UnsupportedOperationException("Not implemented in fake");
}
@Override
public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
throw new UnsupportedOperationException("Not implemented in fake");
}
public void clear() {
store.clear();
}
}

View File

@@ -0,0 +1,18 @@
package com.indie.support;
import org.springframework.security.crypto.password.PasswordEncoder;
public class StubPasswordEncoder implements PasswordEncoder {
private static final String PREFIX = "{stub}";
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + rawPassword;
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(PREFIX + rawPassword);
}
}

View File

@@ -0,0 +1,38 @@
package com.indie.support;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class TestOAuth2User implements OAuth2User {
private final Map<String, Object> attributes;
public TestOAuth2User(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getName() {
Object sub = attributes.get("sub");
if (sub != null) return sub.toString();
Object id = attributes.get("id");
if (id != null) return id.toString();
return "test-user";
}
}

View File

@@ -0,0 +1,21 @@
package com.indie.support;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@TestConfiguration
public class TestSecurityConfig {
@Bean
public SecurityFilterChain testFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}