diff --git a/backend/src/test/java/com/krrishg/config/EncryptionServiceTest.java b/backend/src/test/java/com/krrishg/config/EncryptionServiceTest.java new file mode 100644 index 0000000..e9469ab --- /dev/null +++ b/backend/src/test/java/com/krrishg/config/EncryptionServiceTest.java @@ -0,0 +1,102 @@ +package com.krrishg.config; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class EncryptionServiceTest { + + private Path tempKeyPath; + private EncryptionService service; + + @BeforeEach + void setUp() { + try { + tempKeyPath = Files.createTempFile("test-encryption", ".key"); + service = new EncryptionService(tempKeyPath.toString()); + service.init(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @AfterEach + void tearDown() throws IOException { + Files.deleteIfExists(tempKeyPath); + } + + @Test + void encryptAndDecryptRoundtrip() { + String plaintext = "Hello, World!"; + + String encrypted = service.encrypt(plaintext); + assertNotNull(encrypted); + assertNotEquals(plaintext, encrypted); + + String decrypted = service.decrypt(encrypted); + assertEquals(plaintext, decrypted); + } + + @Test + void encryptAndDecryptSpecialCharacters() { + String plaintext = "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?`~\n\t"; + + String encrypted = service.encrypt(plaintext); + String decrypted = service.decrypt(encrypted); + + assertEquals(plaintext, decrypted); + } + + @Test + void encryptAndDecryptEmptyString() { + String encrypted = service.encrypt(""); + String decrypted = service.decrypt(encrypted); + + assertEquals("", decrypted); + } + + @Test + void differentPlaintextsProduceDifferentCiphertexts() { + String encrypted1 = service.encrypt("text1"); + String encrypted2 = service.encrypt("text2"); + + assertNotEquals(encrypted1, encrypted2); + } + + @Test + void samePlaintextProducedDifferentCiphertextsDueToRandomIv() { + String encrypted1 = service.encrypt("same text"); + String encrypted2 = service.encrypt("same text"); + + assertNotEquals(encrypted1, encrypted2); + } + + @Test + void decryptThrowsOnInvalidData() { + assertThrows(RuntimeException.class, () -> service.decrypt("invalid-base64!")); + } + + @Test + void initCreatesKeyFile() throws IOException { + assertTrue(Files.exists(tempKeyPath)); + assertTrue(Files.size(tempKeyPath) > 0); + } + + @Test + void initLoadsExistingKeyFile() { + EncryptionService secondService = new EncryptionService(tempKeyPath.toString()); + secondService.init(); + + String plaintext = "Persistent key test"; + String encrypted = secondService.encrypt(plaintext); + String decrypted = secondService.decrypt(encrypted); + + assertEquals(plaintext, decrypted); + } +} diff --git a/backend/src/test/java/com/krrishg/config/GitConfigTest.java b/backend/src/test/java/com/krrishg/config/GitConfigTest.java new file mode 100644 index 0000000..41e6220 --- /dev/null +++ b/backend/src/test/java/com/krrishg/config/GitConfigTest.java @@ -0,0 +1,66 @@ +package com.krrishg.config; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class GitConfigTest { + + @Test + void getSiteRepoPathResolvesCorrectly() throws IOException { + Path tempDir = Files.createTempDirectory("git-config-test"); + GitConfig config = new GitConfig(); + setField(config, "reposPath", tempDir.toString()); + setField(config, "authorName", "Indie Platform"); + setField(config, "authorEmail", "git@indie.local"); + config.init(); + + Path repoPath = config.getSiteRepoPath("test-site-id"); + + assertEquals(tempDir.resolve("test-site-id"), repoPath); + assertEquals("Indie Platform", config.getAuthorName()); + assertEquals("git@indie.local", config.getAuthorEmail()); + + Files.deleteIfExists(tempDir); + } + + @Test + void initCreatesDirectory() throws IOException { + Path tempDir = Files.createTempDirectory("git-config-init-test"); + Path reposDir = tempDir.resolve("repos"); + GitConfig config = new GitConfig(); + setField(config, "reposPath", reposDir.toString()); + + assertFalse(Files.exists(reposDir)); + config.init(); + assertTrue(Files.exists(reposDir)); + + Files.deleteIfExists(reposDir); + Files.deleteIfExists(tempDir); + } + + @Test + void settersUpdateAuthorInfo() { + GitConfig config = new GitConfig(); + + config.setAuthorName("Custom Author"); + config.setAuthorEmail("custom@example.com"); + + assertEquals("Custom Author", config.getAuthorName()); + assertEquals("custom@example.com", config.getAuthorEmail()); + } + + private void setField(Object target, String fieldName, Object value) { + try { + var field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/src/test/java/com/krrishg/config/MapToJsonConverterTest.java b/backend/src/test/java/com/krrishg/config/MapToJsonConverterTest.java new file mode 100644 index 0000000..40754c1 --- /dev/null +++ b/backend/src/test/java/com/krrishg/config/MapToJsonConverterTest.java @@ -0,0 +1,73 @@ +package com.krrishg.config; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class MapToJsonConverterTest { + + private final MapToJsonConverter converter = new MapToJsonConverter(); + + @Test + void convertToDatabaseColumnSerializesMap() { + Map map = Map.of("key1", "value1", "key2", "value2"); + + String json = converter.convertToDatabaseColumn(map); + + assertNotNull(json); + assertTrue(json.contains("key1")); + assertTrue(json.contains("value1")); + assertTrue(json.contains("key2")); + assertTrue(json.contains("value2")); + } + + @Test + void convertToEntityAttributeDeserializesJson() { + String json = "{\"a\":\"1\",\"b\":\"2\"}"; + + Map result = converter.convertToEntityAttribute(json); + + assertEquals(2, result.size()); + assertEquals("1", result.get("a")); + assertEquals("2", result.get("b")); + } + + @Test + void roundtripPreservesMap() { + Map original = Map.of("alpha", "beta", "gamma", "delta"); + + String json = converter.convertToDatabaseColumn(original); + Map result = converter.convertToEntityAttribute(json); + + assertEquals(original, result); + } + + @Test + void convertToDatabaseColumnReturnsNullForNullInput() { + assertNull(converter.convertToDatabaseColumn(null)); + } + + @Test + void convertToDatabaseColumnReturnsNullForEmptyMap() { + assertNull(converter.convertToDatabaseColumn(Map.of())); + } + + @Test + void convertToEntityAttributeReturnsEmptyMapForNullInput() { + assertEquals(Map.of(), converter.convertToEntityAttribute(null)); + } + + @Test + void convertToEntityAttributeReturnsEmptyMapForBlankInput() { + assertEquals(Map.of(), converter.convertToEntityAttribute("")); + assertEquals(Map.of(), converter.convertToEntityAttribute(" ")); + } + + @Test + void convertToEntityAttributeThrowsOnMalformedJson() { + assertThrows(RuntimeException.class, () -> + converter.convertToEntityAttribute("{invalid json}")); + } +} diff --git a/backend/src/test/java/com/krrishg/controller/UserSettingsControllerTest.java b/backend/src/test/java/com/krrishg/controller/UserSettingsControllerTest.java new file mode 100644 index 0000000..3f6e971 --- /dev/null +++ b/backend/src/test/java/com/krrishg/controller/UserSettingsControllerTest.java @@ -0,0 +1,254 @@ +package com.krrishg.controller; + +import com.krrishg.config.EncryptionService; +import com.krrishg.config.GlobalExceptionHandler; +import com.krrishg.config.JwtAuthenticationToken; +import com.krrishg.config.UserPrincipal; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; +import com.krrishg.repository.UserRepository; +import com.krrishg.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.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +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(UserSettingsController.class) +@Import({GlobalExceptionHandler.class, TestSecurityConfig.class}) +class UserSettingsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private UserRepository userRepository; + + @MockBean + private EncryptionService encryptionService; + + private UUID userId; + private UserPrincipal principal; + private JwtAuthenticationToken auth; + private User testUser; + + @BeforeEach + void setUp() { + userId = UUID.randomUUID(); + principal = new UserPrincipal(userId, "user@test.com", "Test User"); + auth = new JwtAuthenticationToken(principal, "test-token"); + testUser = User.builder() + .id(userId) + .email("user@test.com") + .name("Test User") + .provider(AuthProvider.LOCAL) + .build(); + } + + @Test + void getLlmConfigReturnsConfigWithAllFields() throws Exception { + Map keys = new HashMap<>(); + keys.put("gemini", "encrypted-gemini-key"); + Map urls = new HashMap<>(); + urls.put("gemini", "https://custom.gemini.com"); + Map models = new HashMap<>(); + models.put("gemini", "gemini-2.0-flash"); + testUser.setLlmApiKeys(keys); + testUser.setLlmBaseUrls(urls); + testUser.setLlmModels(models); + + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + + mockMvc.perform(get("/api/user/llm-config") + .with(authentication(auth))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.gemini.configured").value(true)) + .andExpect(jsonPath("$.gemini.baseUrl").value("https://custom.gemini.com")) + .andExpect(jsonPath("$.gemini.model").value("gemini-2.0-flash")); + } + + @Test + void getLlmConfigReturnsMinimalConfigWhenNoUrlsOrModels() throws Exception { + Map keys = new HashMap<>(); + keys.put("openai", "encrypted-openai-key"); + testUser.setLlmApiKeys(keys); + + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + + mockMvc.perform(get("/api/user/llm-config") + .with(authentication(auth))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.openai.configured").value(true)) + .andExpect(jsonPath("$.openai.baseUrl").doesNotExist()) + .andExpect(jsonPath("$.openai.model").doesNotExist()); + } + + @Test + void getLlmConfigReturnsEmptyMapWhenNoKeys() throws Exception { + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + + mockMvc.perform(get("/api/user/llm-config") + .with(authentication(auth))) + .andExpect(status().isOk()) + .andExpect(content().json("{}")); + } + + @Test + void getLlmConfigReturns400WhenUserNotFound() throws Exception { + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + mockMvc.perform(get("/api/user/llm-config") + .with(authentication(auth))) + .andExpect(status().isBadRequest()); + } + + @Test + void saveLlmConfigSavesApiKey() throws Exception { + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + when(encryptionService.encrypt("sk-test-key")).thenReturn("encrypted-key"); + + Map request = new HashMap<>(); + request.put("provider", "openai"); + request.put("apiKey", "sk-test-key"); + + mockMvc.perform(put("/api/user/llm-config") + .with(authentication(auth)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + + verify(userRepository).save(any(User.class)); + } + + @Test + void saveLlmConfigSavesBaseUrlAndModel() throws Exception { + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + when(encryptionService.encrypt("key")).thenReturn("encrypted"); + + Map request = new HashMap<>(); + request.put("provider", "gemini"); + request.put("apiKey", "key"); + request.put("baseUrl", "https://gemini.api.com"); + request.put("model", "gemini-2.0-flash"); + + mockMvc.perform(put("/api/user/llm-config") + .with(authentication(auth)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + + verify(userRepository).save(any(User.class)); + } + + @Test + void saveLlmConfigMergesWithExistingConfig() throws Exception { + Map existingKeys = new HashMap<>(); + existingKeys.put("gemini", "existing-gemini-key"); + testUser.setLlmApiKeys(existingKeys); + testUser.setLlmBaseUrls(new HashMap<>(Map.of("gemini", "https://gemini.api.com"))); + + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + when(encryptionService.encrypt("new-openai-key")).thenReturn("encrypted-new-key"); + + Map request = new HashMap<>(); + request.put("provider", "openai"); + request.put("apiKey", "new-openai-key"); + + mockMvc.perform(put("/api/user/llm-config") + .with(authentication(auth)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + + verify(userRepository).save(any(User.class)); + } + + @Test + void saveLlmConfigReturns400WhenProviderIsBlank() throws Exception { + Map request = new HashMap<>(); + request.put("provider", ""); + request.put("apiKey", "key"); + + mockMvc.perform(put("/api/user/llm-config") + .with(authentication(auth)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void saveLlmConfigReturns400WhenProviderIsNull() throws Exception { + Map request = new HashMap<>(); + request.put("apiKey", "key"); + + mockMvc.perform(put("/api/user/llm-config") + .with(authentication(auth)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void saveLlmConfigSkipsEmptyApiKey() throws Exception { + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + + Map request = new HashMap<>(); + request.put("provider", "openai"); + request.put("apiKey", ""); + + mockMvc.perform(put("/api/user/llm-config") + .with(authentication(auth)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + + assertNull(testUser.getLlmApiKeys()); + } + + @Test + void deleteLlmConfigRemovesProviderConfig() throws Exception { + Map keys = new HashMap<>(); + keys.put("gemini", "encrypted-key"); + keys.put("openai", "encrypted-key"); + testUser.setLlmApiKeys(keys); + testUser.setLlmBaseUrls(new HashMap<>(Map.of("gemini", "url"))); + testUser.setLlmModels(new HashMap<>(Map.of("gemini", "model"))); + + when(userRepository.findById(userId)).thenReturn(Optional.of(testUser)); + + mockMvc.perform(delete("/api/user/llm-config/gemini") + .with(authentication(auth))) + .andExpect(status().isOk()); + + verify(userRepository).save(any(User.class)); + } + + @Test + void deleteLlmConfigReturns400WhenUserNotFound() throws Exception { + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + mockMvc.perform(delete("/api/user/llm-config/openai") + .with(authentication(auth))) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/com/krrishg/dto/AuthResponseTest.java b/backend/src/test/java/com/krrishg/dto/AuthResponseTest.java new file mode 100644 index 0000000..fc4232b --- /dev/null +++ b/backend/src/test/java/com/krrishg/dto/AuthResponseTest.java @@ -0,0 +1,62 @@ +package com.krrishg.dto; + +import com.krrishg.model.AuthProvider; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class AuthResponseTest { + + @Test + void constructorSetsBearerTokenType() { + AuthResponse.UserInfo userInfo = new AuthResponse.UserInfo( + UUID.randomUUID(), "test@example.com", "Test", null, AuthProvider.LOCAL); + + AuthResponse response = new AuthResponse("access-token", "refresh-token", userInfo); + + assertEquals("access-token", response.getAccessToken()); + assertEquals("refresh-token", response.getRefreshToken()); + assertEquals("Bearer", response.getTokenType()); + assertSame(userInfo, response.getUser()); + } + + @Test + void builderWorksWithTokenType() { + AuthResponse.UserInfo userInfo = AuthResponse.UserInfo.builder() + .id(UUID.randomUUID()) + .email("user@example.com") + .name("User") + .provider(AuthProvider.GOOGLE) + .build(); + + AuthResponse response = AuthResponse.builder() + .accessToken("at") + .refreshToken("rt") + .tokenType("Custom") + .user(userInfo) + .build(); + + assertEquals("Custom", response.getTokenType()); + assertEquals("user@example.com", response.getUser().getEmail()); + } + + @Test + void userInfoBuilderSetsAllFields() { + UUID id = UUID.randomUUID(); + AuthResponse.UserInfo userInfo = AuthResponse.UserInfo.builder() + .id(id) + .email("avatar@example.com") + .name("Avatar User") + .avatarUrl("https://example.com/avatar.png") + .provider(AuthProvider.GITHUB) + .build(); + + assertEquals(id, userInfo.getId()); + assertEquals("avatar@example.com", userInfo.getEmail()); + assertEquals("Avatar User", userInfo.getName()); + assertEquals("https://example.com/avatar.png", userInfo.getAvatarUrl()); + assertEquals(AuthProvider.GITHUB, userInfo.getProvider()); + } +} diff --git a/backend/src/test/java/com/krrishg/dto/LLMConfigRequestTest.java b/backend/src/test/java/com/krrishg/dto/LLMConfigRequestTest.java new file mode 100644 index 0000000..fe08875 --- /dev/null +++ b/backend/src/test/java/com/krrishg/dto/LLMConfigRequestTest.java @@ -0,0 +1,38 @@ +package com.krrishg.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class LLMConfigRequestTest { + + @Test + void compactConstructorSetsProviderAndApiKey() { + LLMConfigRequest request = new LLMConfigRequest("gemini", "test-key"); + + assertEquals("gemini", request.provider()); + assertEquals("test-key", request.apiKey()); + assertNull(request.baseUrl()); + assertNull(request.model()); + } + + @Test + void threeArgConstructorSetsProviderApiKeyAndBaseUrl() { + LLMConfigRequest request = new LLMConfigRequest("openai", "sk-test", "https://api.openai.com"); + + assertEquals("openai", request.provider()); + assertEquals("sk-test", request.apiKey()); + assertEquals("https://api.openai.com", request.baseUrl()); + assertNull(request.model()); + } + + @Test + void fullConstructorSetsAllFields() { + LLMConfigRequest request = new LLMConfigRequest("gemini", "key", "https://custom.url", "gemini-2.0-flash"); + + assertEquals("gemini", request.provider()); + assertEquals("key", request.apiKey()); + assertEquals("https://custom.url", request.baseUrl()); + assertEquals("gemini-2.0-flash", request.model()); + } +} diff --git a/backend/src/test/java/com/krrishg/dto/LLMOptionsTest.java b/backend/src/test/java/com/krrishg/dto/LLMOptionsTest.java new file mode 100644 index 0000000..c3f29bc --- /dev/null +++ b/backend/src/test/java/com/krrishg/dto/LLMOptionsTest.java @@ -0,0 +1,30 @@ +package com.krrishg.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class LLMOptionsTest { + + @Test + void builderDefaults() { + LLMOptions options = LLMOptions.builder().build(); + + assertEquals(0.9, options.getTemperature()); + assertEquals(4096, options.getMaxTokens()); + assertEquals(60, options.getTimeoutSeconds()); + } + + @Test + void builderOverridesDefaults() { + LLMOptions options = LLMOptions.builder() + .temperature(0.5) + .maxTokens(2048) + .timeoutSeconds(30) + .build(); + + assertEquals(0.5, options.getTemperature()); + assertEquals(2048, options.getMaxTokens()); + assertEquals(30, options.getTimeoutSeconds()); + } +} diff --git a/backend/src/test/java/com/krrishg/dto/ReferenceTest.java b/backend/src/test/java/com/krrishg/dto/ReferenceTest.java new file mode 100644 index 0000000..f54e174 --- /dev/null +++ b/backend/src/test/java/com/krrishg/dto/ReferenceTest.java @@ -0,0 +1,38 @@ +package com.krrishg.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ReferenceTest { + + @Test + void builderSetsAllFields() { + Reference ref = Reference.builder() + .type(Reference.ReferenceType.URL) + .url("https://example.com") + .altText("Example Site") + .build(); + + assertEquals(Reference.ReferenceType.URL, ref.getType()); + assertEquals("https://example.com", ref.getUrl()); + assertEquals("Example Site", ref.getAltText()); + } + + @Test + void builderDefaultsToNull() { + Reference ref = Reference.builder().build(); + + assertNull(ref.getType()); + assertNull(ref.getUrl()); + assertNull(ref.getAltText()); + } + + @Test + void referenceTypeEnumValues() { + assertArrayEquals( + new Reference.ReferenceType[]{Reference.ReferenceType.URL, Reference.ReferenceType.IMAGE}, + Reference.ReferenceType.values() + ); + } +} diff --git a/backend/src/test/java/com/krrishg/dto/SelfHostedConfigResponseTest.java b/backend/src/test/java/com/krrishg/dto/SelfHostedConfigResponseTest.java new file mode 100644 index 0000000..1354af1 --- /dev/null +++ b/backend/src/test/java/com/krrishg/dto/SelfHostedConfigResponseTest.java @@ -0,0 +1,70 @@ +package com.krrishg.dto; + +import com.krrishg.model.SelfHostedConfig; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class SelfHostedConfigResponseTest { + + @Test + void fromMapsAllFields() { + UUID id = UUID.randomUUID(); + UUID siteId = UUID.randomUUID(); + LocalDateTime now = LocalDateTime.now(); + + SelfHostedConfig config = SelfHostedConfig.builder() + .id(id) + .siteId(siteId) + .domain("example.com") + .sshHost("192.168.1.1") + .sshPort(2222) + .sshUser("deploy") + .deployPath("/var/www/html") + .nginxSitesPath("/etc/nginx/conf.d") + .sslEnabled(true) + .sslEmail("admin@example.com") + .deployedAt(now) + .createdAt(now) + .updatedAt(now) + .build(); + + SelfHostedConfigResponse response = SelfHostedConfigResponse.from(config, "ssh-ed25519 AAA..."); + + assertEquals(id, response.getId()); + assertEquals(siteId, response.getSiteId()); + assertEquals("example.com", response.getDomain()); + assertEquals("192.168.1.1", response.getSshHost()); + assertEquals(2222, response.getSshPort()); + assertEquals("deploy", response.getSshUser()); + assertEquals("/var/www/html", response.getDeployPath()); + assertEquals("/etc/nginx/conf.d", response.getNginxSitesPath()); + assertTrue(response.isSslEnabled()); + assertEquals("admin@example.com", response.getSslEmail()); + assertEquals("ssh-ed25519 AAA...", response.getPublicKey()); + assertEquals(now, response.getDeployedAt()); + assertEquals(now, response.getCreatedAt()); + assertEquals(now, response.getUpdatedAt()); + } + + @Test + void fromHandlesNullOptionalFields() { + SelfHostedConfig config = SelfHostedConfig.builder() + .id(UUID.randomUUID()) + .siteId(UUID.randomUUID()) + .domain("example.com") + .sshHost("host") + .sshUser("user") + .build(); + + SelfHostedConfigResponse response = SelfHostedConfigResponse.from(config, null); + + assertNull(response.getSslEmail()); + assertNull(response.getPublicKey()); + assertNull(response.getDeployedAt()); + assertFalse(response.isSslEnabled()); + } +} diff --git a/backend/src/test/java/com/krrishg/dto/SiteStructureTest.java b/backend/src/test/java/com/krrishg/dto/SiteStructureTest.java new file mode 100644 index 0000000..cb88738 --- /dev/null +++ b/backend/src/test/java/com/krrishg/dto/SiteStructureTest.java @@ -0,0 +1,57 @@ +package com.krrishg.dto; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class SiteStructureTest { + + @Test + void builderSetsAllFields() { + Map colors = Map.of("primary", "#000", "secondary", "#fff"); + Map fonts = Map.of("heading", "Arial"); + Map spacing = Map.of("margin", "16px"); + + SiteStructure.ThemeConfig theme = new SiteStructure.ThemeConfig(colors, fonts, spacing); + + SiteStructure.PageStructure page = SiteStructure.PageStructure.builder() + .id("home") + .title("Home") + .slug("/") + .seoTitle("Home Page") + .seoDescription("Welcome") + .orderIndex(0) + .rawHtml("

Hello

") + .build(); + + SiteStructure structure = SiteStructure.builder() + .theme(theme) + .pages(List.of(page)) + .build(); + + assertNotNull(structure.getTheme()); + assertEquals("#000", structure.getTheme().getColors().get("primary")); + assertEquals("Arial", structure.getTheme().getFonts().get("heading")); + assertEquals("16px", structure.getTheme().getSpacing().get("margin")); + + assertEquals(1, structure.getPages().size()); + assertEquals("home", structure.getPages().get(0).getId()); + assertEquals("Home", structure.getPages().get(0).getTitle()); + assertEquals("/", structure.getPages().get(0).getSlug()); + assertEquals("Home Page", structure.getPages().get(0).getSeoTitle()); + assertEquals("Welcome", structure.getPages().get(0).getSeoDescription()); + assertEquals(0, structure.getPages().get(0).getOrderIndex()); + assertEquals("

Hello

", structure.getPages().get(0).getRawHtml()); + } + + @Test + void builderHandlesNullOptionalFields() { + SiteStructure structure = SiteStructure.builder().build(); + + assertNull(structure.getTheme()); + assertNull(structure.getPages()); + } +} diff --git a/backend/src/test/java/com/krrishg/model/AuthProviderTest.java b/backend/src/test/java/com/krrishg/model/AuthProviderTest.java new file mode 100644 index 0000000..bf28103 --- /dev/null +++ b/backend/src/test/java/com/krrishg/model/AuthProviderTest.java @@ -0,0 +1,16 @@ +package com.krrishg.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AuthProviderTest { + + @Test + void enumValues() { + assertArrayEquals( + new AuthProvider[]{AuthProvider.LOCAL, AuthProvider.GOOGLE, AuthProvider.GITHUB, AuthProvider.OPENID}, + AuthProvider.values() + ); + } +} diff --git a/backend/src/test/java/com/krrishg/model/GenerationVersionTest.java b/backend/src/test/java/com/krrishg/model/GenerationVersionTest.java new file mode 100644 index 0000000..980b814 --- /dev/null +++ b/backend/src/test/java/com/krrishg/model/GenerationVersionTest.java @@ -0,0 +1,67 @@ +package com.krrishg.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class GenerationVersionTest { + + @Test + void builderSetsAllFields() { + UUID siteId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + LocalDateTime now = LocalDateTime.now(); + + GenerationVersion version = GenerationVersion.builder() + .id("mongo-id") + .siteId(siteId) + .userId(userId) + .versionNumber(1) + .prompt("Build a landing page") + .fullStructure("{\"theme\":{}}") + .createdAt(now) + .build(); + + assertEquals("mongo-id", version.getId()); + assertEquals(siteId, version.getSiteId()); + assertEquals(userId, version.getUserId()); + assertEquals(1, version.getVersionNumber()); + assertEquals("Build a landing page", version.getPrompt()); + assertEquals("{\"theme\":{}}", version.getFullStructure()); + assertEquals(now, version.getCreatedAt()); + } + + @Test + void builderSupportsReferenceEntries() { + GenerationVersion.ReferenceEntry entry = GenerationVersion.ReferenceEntry.builder() + .type("IMAGE") + .url("https://example.com/img.png") + .altText("Screenshot") + .build(); + + GenerationVersion version = GenerationVersion.builder() + .siteId(UUID.randomUUID()) + .userId(UUID.randomUUID()) + .versionNumber(2) + .prompt("With references") + .references(List.of(entry)) + .build(); + + assertEquals(1, version.getReferences().size()); + assertEquals("IMAGE", version.getReferences().get(0).getType()); + assertEquals("https://example.com/img.png", version.getReferences().get(0).getUrl()); + assertEquals("Screenshot", version.getReferences().get(0).getAltText()); + } + + @Test + void noArgsConstructorCreatesEmptyObject() { + GenerationVersion version = new GenerationVersion(); + assertNull(version.getId()); + assertNull(version.getSiteId()); + assertEquals(0, version.getVersionNumber()); + } +} diff --git a/backend/src/test/java/com/krrishg/model/SelfHostedConfigTest.java b/backend/src/test/java/com/krrishg/model/SelfHostedConfigTest.java new file mode 100644 index 0000000..8ead9ea --- /dev/null +++ b/backend/src/test/java/com/krrishg/model/SelfHostedConfigTest.java @@ -0,0 +1,78 @@ +package com.krrishg.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class SelfHostedConfigTest { + + @Test + void prePersistSetsIdAndTimestamps() { + SelfHostedConfig config = new SelfHostedConfig(); + config.setSiteId(UUID.randomUUID()); + config.setDomain("example.com"); + config.setSshHost("192.168.1.1"); + config.setSshUser("deploy"); + + assertNull(config.getId()); + assertNull(config.getCreatedAt()); + assertNull(config.getUpdatedAt()); + + config.onCreate(); + + assertNotNull(config.getId()); + assertNotNull(config.getCreatedAt()); + assertNotNull(config.getUpdatedAt()); + } + + @Test + void prePersistDoesNotOverrideExistingId() { + UUID existingId = UUID.randomUUID(); + SelfHostedConfig config = SelfHostedConfig.builder() + .id(existingId) + .siteId(UUID.randomUUID()) + .domain("example.com") + .sshHost("host") + .sshUser("user") + .build(); + + config.onCreate(); + + assertEquals(existingId, config.getId()); + } + + @Test + void preUpdateSetsUpdatedAt() { + SelfHostedConfig config = SelfHostedConfig.builder() + .siteId(UUID.randomUUID()) + .domain("example.com") + .sshHost("host") + .sshUser("user") + .build(); + config.onCreate(); + + LocalDateTime original = config.getUpdatedAt(); + + config.onUpdate(); + + assertTrue(config.getUpdatedAt().isAfter(original)); + } + + @Test + void builderSetsDefaults() { + SelfHostedConfig config = SelfHostedConfig.builder() + .siteId(UUID.randomUUID()) + .domain("example.com") + .sshHost("host") + .sshUser("user") + .build(); + + assertEquals(22, config.getSshPort()); + assertEquals("/var/www/{domain}/public", config.getDeployPath()); + assertEquals("/etc/nginx/sites-available", config.getNginxSitesPath()); + assertFalse(config.isSslEnabled()); + } +} diff --git a/backend/src/test/java/com/krrishg/model/SiteStatusTest.java b/backend/src/test/java/com/krrishg/model/SiteStatusTest.java new file mode 100644 index 0000000..52d9966 --- /dev/null +++ b/backend/src/test/java/com/krrishg/model/SiteStatusTest.java @@ -0,0 +1,16 @@ +package com.krrishg.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SiteStatusTest { + + @Test + void enumValues() { + assertArrayEquals( + new SiteStatus[]{SiteStatus.DRAFT, SiteStatus.PUBLISHED}, + SiteStatus.values() + ); + } +} diff --git a/backend/src/test/java/com/krrishg/service/OAuth2SuccessHandlerTest.java b/backend/src/test/java/com/krrishg/service/OAuth2SuccessHandlerTest.java new file mode 100644 index 0000000..65398c9 --- /dev/null +++ b/backend/src/test/java/com/krrishg/service/OAuth2SuccessHandlerTest.java @@ -0,0 +1,76 @@ +package com.krrishg.service; + +import com.krrishg.config.JwtConfig; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.RedirectStrategy; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class OAuth2SuccessHandlerTest { + + @Mock + private JwtConfig jwtConfig; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private Authentication authentication; + + @Mock + private RedirectStrategy redirectStrategy; + + private OAuth2SuccessHandler handler; + + @BeforeEach + void setUp() { + handler = new OAuth2SuccessHandler(jwtConfig); + handler.setRedirectStrategy(redirectStrategy); + } + + @Test + void onAuthenticationSuccessWithOAuth2UserRedirectsWithTokens() throws Exception { + CustomOAuth2User oAuth2User = mock(CustomOAuth2User.class); + User user = User.builder() + .id(UUID.randomUUID()) + .email("user@example.com") + .name("User") + .provider(AuthProvider.GOOGLE) + .build(); + when(authentication.getPrincipal()).thenReturn(oAuth2User); + when(oAuth2User.getUser()).thenReturn(user); + when(jwtConfig.generateAccessToken(user)).thenReturn("access-token-value"); + when(jwtConfig.generateRefreshToken(user)).thenReturn("refresh-token-value"); + + handler.onAuthenticationSuccess(request, response, authentication); + + String expectedRedirect = "http://localhost:4200/oauth2/callback" + + "?accessToken=access-token-value&refreshToken=refresh-token-value"; + verify(redirectStrategy).sendRedirect(request, response, expectedRedirect); + } + + @Test + void onAuthenticationSuccessWithNonOAuth2UserCallsSuper() throws Exception { + when(authentication.getPrincipal()).thenReturn("plain principal"); + + handler.onAuthenticationSuccess(request, response, authentication); + + verify(redirectStrategy).sendRedirect(eq(request), eq(response), anyString()); + } +} diff --git a/backend/src/test/java/com/krrishg/service/llm/ProviderFactoryTest.java b/backend/src/test/java/com/krrishg/service/llm/ProviderFactoryTest.java new file mode 100644 index 0000000..254495b --- /dev/null +++ b/backend/src/test/java/com/krrishg/service/llm/ProviderFactoryTest.java @@ -0,0 +1,105 @@ +package com.krrishg.service.llm; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ProviderFactoryTest { + + @Mock + private RestTemplateBuilder restTemplateBuilder; + + @Mock + private RestTemplate restTemplate; + + private ProviderFactory factory; + + @BeforeEach + void setUp() { + lenient().when(restTemplateBuilder.setConnectTimeout(any(Duration.class))).thenReturn(restTemplateBuilder); + lenient().when(restTemplateBuilder.setReadTimeout(any(Duration.class))).thenReturn(restTemplateBuilder); + lenient().when(restTemplateBuilder.build()).thenReturn(restTemplate); + + factory = new ProviderFactory( + "gemini-2.0-flash", + "https://generativelanguage.googleapis.com/v1beta/models", + "gpt-4o-mini", + "https://api.openai.com/v1/chat/completions", + restTemplateBuilder + ); + } + + @Test + void createProviderReturnsGeminiProvider() { + LLMProvider provider = factory.createProvider("gemini", "test-key"); + + assertInstanceOf(GeminiProvider.class, provider); + assertEquals("gemini", provider.getName()); + } + + @Test + void createProviderReturnsOpenAIProvider() { + LLMProvider provider = factory.createProvider("openai", "sk-test"); + + assertInstanceOf(OpenAIProvider.class, provider); + assertEquals("openai", provider.getName()); + } + + @Test + void createProviderWithCustomBaseUrl() { + LLMProvider provider = factory.createProvider("gemini", "key", "https://custom.url"); + + assertInstanceOf(GeminiProvider.class, provider); + } + + @Test + void createProviderWithCustomBaseUrlAndModel() { + LLMProvider provider = factory.createProvider("openai", "key", "https://custom.openai.com", "gpt-4"); + + assertInstanceOf(OpenAIProvider.class, provider); + } + + @Test + void createProviderWithCustomBaseUrlAppendsChatCompletionsForOpenAI() { + LLMProvider provider = factory.createProvider("openai", "key", "https://custom.openai.com"); + + assertInstanceOf(OpenAIProvider.class, provider); + } + + @Test + void createProviderWithCustomBaseUrlHandlesTrailingSlashForOpenAI() { + LLMProvider provider = factory.createProvider("openai", "key", "https://custom.openai.com/"); + + assertInstanceOf(OpenAIProvider.class, provider); + } + + @Test + void createProviderThrowsOnUnknownProvider() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> factory.createProvider("unknown", "key")); + + assertTrue(ex.getMessage().contains("unknown")); + } + + @Test + void getAvailableProvidersReturnsBothProviders() { + List providers = factory.getAvailableProviders(); + + assertEquals(2, providers.size()); + assertTrue(providers.contains("gemini")); + assertTrue(providers.contains("openai")); + } +}