diff --git a/backend/src/test/java/com/krrishg/config/JwtConfigTest.java b/backend/src/test/java/com/krrishg/config/JwtConfigTest.java new file mode 100644 index 0000000..760fab2 --- /dev/null +++ b/backend/src/test/java/com/krrishg/config/JwtConfigTest.java @@ -0,0 +1,193 @@ +package com.krrishg.config; + +import com.krrishg.model.User; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.util.UUID; + +import static io.jsonwebtoken.Jwts.SIG.RS256; +import static org.junit.jupiter.api.Assertions.*; + +class JwtConfigTest { + + @Test + void generateAndValidateAccessToken() { + JwtConfig jwtConfig = createWithExpiration(900_000L, 604_800_000L); + jwtConfig.init(); + + User user = User.builder() + .id(UUID.randomUUID()) + .email("test@example.com") + .name("Test User") + .build(); + + String token = jwtConfig.generateAccessToken(user); + Claims claims = jwtConfig.validateToken(token); + + assertEquals(user.getId().toString(), claims.getSubject()); + assertEquals("test@example.com", claims.get("email", String.class)); + assertEquals("Test User", claims.get("name", String.class)); + assertNotNull(claims.getIssuedAt()); + assertNotNull(claims.getExpiration()); + } + + @Test + void generateAndValidateRefreshToken() { + JwtConfig jwtConfig = createWithExpiration(900_000L, 604_800_000L); + jwtConfig.init(); + + User user = User.builder() + .id(UUID.randomUUID()) + .email("refresh@example.com") + .name("Refresh User") + .build(); + + String token = jwtConfig.generateRefreshToken(user); + Claims claims = jwtConfig.validateToken(token); + + assertEquals(user.getId().toString(), claims.getSubject()); + assertEquals("refresh@example.com", claims.get("email", String.class)); + assertEquals("Refresh User", claims.get("name", String.class)); + } + + @Test + void multipleTokensForSameUserAreAllValid() { + JwtConfig jwtConfig = createWithExpiration(900_000L, 604_800_000L); + jwtConfig.init(); + + User user = User.builder() + .id(UUID.randomUUID()) + .email("same@example.com") + .build(); + + String token1 = jwtConfig.generateAccessToken(user); + String token2 = jwtConfig.generateAccessToken(user); + + assertDoesNotThrow(() -> jwtConfig.validateToken(token1)); + assertDoesNotThrow(() -> jwtConfig.validateToken(token2)); + Claims claims1 = jwtConfig.validateToken(token1); + Claims claims2 = jwtConfig.validateToken(token2); + assertEquals(user.getId().toString(), claims1.getSubject()); + assertEquals(user.getId().toString(), claims2.getSubject()); + } + + @Test + void rejectTamperedToken() { + JwtConfig jwtConfig = createWithExpiration(900_000L, 604_800_000L); + jwtConfig.init(); + + User user = User.builder() + .id(UUID.randomUUID()) + .email("tamper@example.com") + .build(); + + String token = jwtConfig.generateAccessToken(user); + String tampered = token.substring(0, token.lastIndexOf('.') + 1) + "tampered"; + + assertThrows(Exception.class, () -> jwtConfig.validateToken(tampered)); + } + + @Test + void rejectExpiredToken() throws Exception { + JwtConfig jwtConfig = new JwtConfig(); + setField(jwtConfig, "accessTokenExpiration", -1000L); + jwtConfig.init(); + + User user = User.builder() + .id(UUID.randomUUID()) + .email("expired@example.com") + .build(); + + String token = jwtConfig.generateAccessToken(user); + + assertThrows(Exception.class, () -> jwtConfig.validateToken(token)); + } + + @Test + void initGeneratesEphemeralKeys() { + JwtConfig jwtConfig = createWithExpiration(900_000L, 604_800_000L); + jwtConfig.init(); + + assertNotNull(jwtConfig.getPublicKey()); + User user = User.builder().id(UUID.randomUUID()).email("ephemeral@test.com").build(); + String token = jwtConfig.generateAccessToken(user); + assertDoesNotThrow(() -> jwtConfig.validateToken(token)); + } + + @Test + void initLoadsKeysFromFiles(@TempDir Path tempDir) throws Exception { + KeyPair keyPair = RS256.keyPair().build(); + Path privPath = tempDir.resolve("private.key"); + Path pubPath = tempDir.resolve("public.key"); + Files.write(privPath, keyPair.getPrivate().getEncoded()); + Files.write(pubPath, keyPair.getPublic().getEncoded()); + + JwtConfig jwtConfig = new JwtConfig(); + setField(jwtConfig, "privateKeyPath", privPath.toString()); + setField(jwtConfig, "publicKeyPath", pubPath.toString()); + setField(jwtConfig, "accessTokenExpiration", 900_000L); + setField(jwtConfig, "refreshTokenExpiration", 604_800_000L); + jwtConfig.init(); + + User user = User.builder().id(UUID.randomUUID()).email("file@test.com").name("File Test").build(); + String token = jwtConfig.generateAccessToken(user); + Claims claims = jwtConfig.validateToken(token); + assertEquals(user.getId().toString(), claims.getSubject()); + assertEquals("file@test.com", claims.get("email", String.class)); + assertEquals("File Test", claims.get("name", String.class)); + } + + @Test + void initGeneratesAndSavesKeysWhenFilesDoNotExist(@TempDir Path tempDir) throws Exception { + Path privPath = tempDir.resolve("private.key"); + Path pubPath = tempDir.resolve("public.key"); + + JwtConfig jwtConfig = new JwtConfig(); + setField(jwtConfig, "privateKeyPath", privPath.toString()); + setField(jwtConfig, "publicKeyPath", pubPath.toString()); + setField(jwtConfig, "accessTokenExpiration", 900_000L); + setField(jwtConfig, "refreshTokenExpiration", 604_800_000L); + jwtConfig.init(); + + assertTrue(Files.exists(privPath)); + assertTrue(Files.exists(pubPath)); + + User user = User.builder().id(UUID.randomUUID()).email("saved@test.com").build(); + String token = jwtConfig.generateAccessToken(user); + assertDoesNotThrow(() -> jwtConfig.validateToken(token)); + } + + @Test + void getExpirationReturnsConfiguredValues() throws Exception { + JwtConfig jwtConfig = new JwtConfig(); + setField(jwtConfig, "accessTokenExpiration", 300_000L); + setField(jwtConfig, "refreshTokenExpiration", 1_800_000L); + + assertEquals(300_000L, jwtConfig.getAccessTokenExpiration()); + assertEquals(1_800_000L, jwtConfig.getRefreshTokenExpiration()); + } + + private JwtConfig createWithExpiration(long access, long refresh) { + JwtConfig jwtConfig = new JwtConfig(); + try { + setField(jwtConfig, "accessTokenExpiration", access); + setField(jwtConfig, "refreshTokenExpiration", refresh); + } catch (Exception e) { + throw new RuntimeException(e); + } + return jwtConfig; + } + + private void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/backend/src/test/java/com/krrishg/config/SchemaMigrationTest.java b/backend/src/test/java/com/krrishg/config/SchemaMigrationTest.java new file mode 100644 index 0000000..36c190c --- /dev/null +++ b/backend/src/test/java/com/krrishg/config/SchemaMigrationTest.java @@ -0,0 +1,60 @@ +package com.krrishg.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class SchemaMigrationTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private DataSource dataSource; + + @Test + void migrateExecutesWithoutError() { + SchemaMigration migration = new SchemaMigration(jdbcTemplate); + assertDoesNotThrow(() -> migration.migrate()); + } + + @Test + void migrateIsIdempotent() { + SchemaMigration migration = new SchemaMigration(jdbcTemplate); + migration.migrate(); + assertDoesNotThrow(() -> migration.migrate()); + } + + @Test + void sslColumnsExistOnSelfHostedConfigs() throws SQLException { + SchemaMigration migration = new SchemaMigration(jdbcTemplate); + migration.migrate(); + + assertTrue(columnExists("self_hosted_configs", "ssl_enabled")); + assertTrue(columnExists("self_hosted_configs", "ssl_email")); + } + + @Test + void encryptedSshKeyColumnIsDropped() throws SQLException { + SchemaMigration migration = new SchemaMigration(jdbcTemplate); + migration.migrate(); + + assertFalse(columnExists("self_hosted_configs", "encrypted_ssh_key")); + } + + private boolean columnExists(String table, String column) throws SQLException { + try (var rs = dataSource.getConnection().getMetaData() + .getColumns(null, null, table, column)) { + return rs.next(); + } + } +} diff --git a/backend/src/test/java/com/krrishg/config/SecurityConfigTest.java b/backend/src/test/java/com/krrishg/config/SecurityConfigTest.java new file mode 100644 index 0000000..4cefeb2 --- /dev/null +++ b/backend/src/test/java/com/krrishg/config/SecurityConfigTest.java @@ -0,0 +1,144 @@ +package com.krrishg.config; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.AfterEach; +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.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.lang.reflect.Method; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SecurityConfigTest { + + @Mock + private JwtConfig jwtConfig; + + private SecurityConfig securityConfig; + + @BeforeEach + void setUp() { + securityConfig = new SecurityConfig(jwtConfig); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void setsAuthenticationForValidToken() throws Exception { + UUID userId = UUID.randomUUID(); + Claims claims = mock(Claims.class); + when(claims.getSubject()).thenReturn(userId.toString()); + when(claims.get("email", String.class)).thenReturn("test@example.com"); + when(claims.get("name", String.class)).thenReturn("Test User"); + when(jwtConfig.validateToken("valid-token")).thenReturn(claims); + + OncePerRequestFilter filter = getJwtFilter(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("Bearer valid-token"); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + assertInstanceOf(JwtAuthenticationToken.class, auth); + UserPrincipal principal = (UserPrincipal) auth.getPrincipal(); + assertEquals(userId, principal.id()); + assertEquals("test@example.com", principal.email()); + assertEquals("Test User", principal.name()); + verify(chain).doFilter(request, response); + } + + @Test + void clearsContextOnInvalidToken() throws Exception { + when(jwtConfig.validateToken(anyString())).thenThrow(new RuntimeException("bad token")); + + OncePerRequestFilter filter = getJwtFilter(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("Bearer bad-token"); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + verify(chain).doFilter(request, response); + } + + @Test + void doesNotAuthenticateWhenNoAuthHeader() throws Exception { + OncePerRequestFilter filter = getJwtFilter(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn(null); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + verify(chain).doFilter(request, response); + verify(jwtConfig, never()).validateToken(any()); + } + + @Test + void doesNotAuthenticateForNonBearerScheme() throws Exception { + OncePerRequestFilter filter = getJwtFilter(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("Basic credentials"); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + verify(chain).doFilter(request, response); + verify(jwtConfig, never()).validateToken(any()); + } + + @Test + void filterCallsDoFilterOnChain() throws Exception { + OncePerRequestFilter filter = getJwtFilter(); + var request = mock(HttpServletRequest.class); + var response = mock(HttpServletResponse.class); + var chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + } + + @Test + void alwaysCallsDoFilterEvenOnException() throws Exception { + OncePerRequestFilter filter = getJwtFilter(); + var request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("Bearer token"); + when(jwtConfig.validateToken(anyString())).thenThrow(new RuntimeException("fail")); + var response = mock(HttpServletResponse.class); + var chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + } + + private OncePerRequestFilter getJwtFilter() throws Exception { + Method method = SecurityConfig.class.getDeclaredMethod("jwtAuthenticationFilter"); + method.setAccessible(true); + return (OncePerRequestFilter) method.invoke(securityConfig); + } +} diff --git a/backend/src/test/java/com/krrishg/config/StorageConfigTest.java b/backend/src/test/java/com/krrishg/config/StorageConfigTest.java new file mode 100644 index 0000000..2ea5f73 --- /dev/null +++ b/backend/src/test/java/com/krrishg/config/StorageConfigTest.java @@ -0,0 +1,30 @@ +package com.krrishg.config; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.s3.S3Client; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; + +class StorageConfigTest { + + @Test + void s3ClientIsCreatedWithConfiguration() throws Exception { + StorageConfig config = new StorageConfig(); + setField(config, "endpoint", "http://localhost:9000"); + setField(config, "region", "us-east-1"); + setField(config, "accessKey", "test-access-key"); + setField(config, "secretKey", "test-secret-key"); + + S3Client client = config.s3Client(); + assertNotNull(client); + client.close(); + } + + private void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/backend/src/test/java/com/krrishg/service/CleanupServiceTest.java b/backend/src/test/java/com/krrishg/service/CleanupServiceTest.java new file mode 100644 index 0000000..c5e9bf5 --- /dev/null +++ b/backend/src/test/java/com/krrishg/service/CleanupServiceTest.java @@ -0,0 +1,165 @@ +package com.krrishg.service; + +import com.krrishg.model.User; +import com.krrishg.model.VerificationToken; +import com.krrishg.support.FakeUserRepository; +import com.krrishg.support.FakeVerificationTokenRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class CleanupServiceTest { + + private FakeUserRepository userRepository; + private FakeVerificationTokenRepository tokenRepository; + private CleanupService cleanupService; + + @BeforeEach + void setUp() { + userRepository = new FakeUserRepository(); + tokenRepository = new FakeVerificationTokenRepository(); + cleanupService = new CleanupService(userRepository, tokenRepository); + } + + @Test + void deletesUnverifiedUserWithExpiredToken() { + User user = User.builder() + .email("stale@example.com") + .emailVerified(false) + .build(); + user.onCreate(); + userRepository.save(user); + + VerificationToken token = VerificationToken.builder() + .userId(user.getId()) + .token("expired-token") + .used(false) + .expiryDate(LocalDateTime.now().minusDays(3)) + .build(); + tokenRepository.save(token); + + cleanupService.cleanupStaleUnverifiedAccounts(); + + assertTrue(userRepository.findById(user.getId()).isEmpty()); + assertTrue(tokenRepository.findByToken("expired-token").isEmpty()); + } + + @Test + void preservesVerifiedUserWithExpiredToken() { + User user = User.builder() + .email("verified@example.com") + .emailVerified(true) + .build(); + user.onCreate(); + userRepository.save(user); + + VerificationToken token = VerificationToken.builder() + .userId(user.getId()) + .token("verified-expired") + .used(false) + .expiryDate(LocalDateTime.now().minusDays(3)) + .build(); + tokenRepository.save(token); + + cleanupService.cleanupStaleUnverifiedAccounts(); + + assertTrue(userRepository.findById(user.getId()).isPresent()); + assertTrue(tokenRepository.findByToken("verified-expired").isPresent()); + } + + @Test + void preservesUnverifiedUserWithValidToken() { + User user = User.builder() + .email("pending@example.com") + .emailVerified(false) + .build(); + user.onCreate(); + userRepository.save(user); + + VerificationToken token = VerificationToken.builder() + .userId(user.getId()) + .token("valid-token") + .used(false) + .expiryDate(LocalDateTime.now().plusDays(1)) + .build(); + tokenRepository.save(token); + + cleanupService.cleanupStaleUnverifiedAccounts(); + + assertTrue(userRepository.findById(user.getId()).isPresent()); + assertTrue(tokenRepository.findByToken("valid-token").isPresent()); + } + + @Test + void doesNothingWhenNoStaleTokens() { + cleanupService.cleanupStaleUnverifiedAccounts(); + + assertEquals(0, userRepository.count()); + assertEquals(0, tokenRepository.count()); + } + + @Test + void cleansUpMultipleStaleUsers() { + User user1 = User.builder() + .email("stale1@example.com") + .emailVerified(false) + .build(); + user1.onCreate(); + userRepository.save(user1); + + User user2 = User.builder() + .email("stale2@example.com") + .emailVerified(false) + .build(); + user2.onCreate(); + userRepository.save(user2); + + VerificationToken token1 = VerificationToken.builder() + .userId(user1.getId()) + .token("stale-token-1") + .used(false) + .expiryDate(LocalDateTime.now().minusDays(3)) + .build(); + tokenRepository.save(token1); + + VerificationToken token2 = VerificationToken.builder() + .userId(user2.getId()) + .token("stale-token-2") + .used(false) + .expiryDate(LocalDateTime.now().minusDays(3)) + .build(); + tokenRepository.save(token2); + + cleanupService.cleanupStaleUnverifiedAccounts(); + + assertTrue(userRepository.findById(user1.getId()).isEmpty()); + assertTrue(userRepository.findById(user2.getId()).isEmpty()); + assertTrue(tokenRepository.findByToken("stale-token-1").isEmpty()); + assertTrue(tokenRepository.findByToken("stale-token-2").isEmpty()); + } + + @Test + void skipsUsedToken() { + User user = User.builder() + .email("used-token@example.com") + .emailVerified(false) + .build(); + user.onCreate(); + userRepository.save(user); + + VerificationToken token = VerificationToken.builder() + .userId(user.getId()) + .token("used-token") + .used(true) + .expiryDate(LocalDateTime.now().minusDays(3)) + .build(); + tokenRepository.save(token); + + cleanupService.cleanupStaleUnverifiedAccounts(); + + assertTrue(userRepository.findById(user.getId()).isPresent()); + } +} diff --git a/backend/src/test/java/com/krrishg/service/EmailServiceTest.java b/backend/src/test/java/com/krrishg/service/EmailServiceTest.java new file mode 100644 index 0000000..6d06827 --- /dev/null +++ b/backend/src/test/java/com/krrishg/service/EmailServiceTest.java @@ -0,0 +1,83 @@ +package com.krrishg.service; + +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +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.mail.javamail.JavaMailSender; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EmailServiceTest { + + @Mock + private JavaMailSender mailSender; + + private EmailService emailService; + + @BeforeEach + void setUp() { + emailService = new EmailService(mailSender, "http://localhost:4200", "noreply@indie.app"); + } + + @Test + void sendsEmailWithCorrectRecipientsAndSubject() throws Exception { + MimeMessage message = new MimeMessage(Session.getDefaultInstance(new Properties())); + when(mailSender.createMimeMessage()).thenReturn(message); + + emailService.sendVerificationEmail("user@example.com", "User", "token123"); + + verify(mailSender).send(message); + String raw = writeMessage(message); + assertTrue(raw.contains("From: noreply@indie.app")); + assertTrue(raw.contains("To: user@example.com")); + assertTrue(raw.contains("Subject: Verify your Indie account")); + } + + @Test + void emailBodyContainsVerificationLink() throws Exception { + MimeMessage message = new MimeMessage(Session.getDefaultInstance(new Properties())); + when(mailSender.createMimeMessage()).thenReturn(message); + + emailService.sendVerificationEmail("user@example.com", "User", "test-token-123"); + + String raw = writeMessage(message); + assertTrue(raw.contains("http://localhost:4200/verify-email?token=test-token-123")); + assertTrue(raw.contains("Hi User,")); + } + + @Test + void usesEmailPrefixWhenNameIsNull() throws Exception { + MimeMessage message = new MimeMessage(Session.getDefaultInstance(new Properties())); + when(mailSender.createMimeMessage()).thenReturn(message); + + emailService.sendVerificationEmail("john.doe@example.com", null, "token123"); + + String raw = writeMessage(message); + assertTrue(raw.contains("Hi john.doe,")); + } + + @Test + void propagatesExceptionFromMailSender() { + when(mailSender.createMimeMessage()).thenThrow(new RuntimeException("mail error")); + + RuntimeException ex = assertThrows(RuntimeException.class, + () -> emailService.sendVerificationEmail("user@example.com", "User", "token123")); + assertEquals("mail error", ex.getMessage()); + } + + private String writeMessage(MimeMessage message) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + message.writeTo(baos); + return baos.toString(StandardCharsets.UTF_8); + } +} diff --git a/backend/src/test/java/com/krrishg/service/SshRsyncDeployerTest.java b/backend/src/test/java/com/krrishg/service/SshRsyncDeployerTest.java index ffb61fa..81957e8 100644 --- a/backend/src/test/java/com/krrishg/service/SshRsyncDeployerTest.java +++ b/backend/src/test/java/com/krrishg/service/SshRsyncDeployerTest.java @@ -1,13 +1,19 @@ package com.krrishg.service; import com.krrishg.model.SelfHostedConfig; +import com.krrishg.service.RenderService.SiteOutput; 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 java.io.IOException; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -48,6 +54,30 @@ class SshRsyncDeployerTest { assertEquals("https://example.com/", url); } + @Test + void buildUrlHandlesDomainWithTrailingSlash() { + SelfHostedConfig config = SelfHostedConfig.builder() + .domain("example.com") + .sslEnabled(true) + .build(); + + String url = deployer.buildUrl(config); + + assertEquals("https://example.com/", url); + } + + @Test + void buildUrlHandlesIpAddress() { + SelfHostedConfig config = SelfHostedConfig.builder() + .domain("192.168.1.100") + .sslEnabled(false) + .build(); + + String url = deployer.buildUrl(config); + + assertEquals("http://192.168.1.100/", url); + } + @Test void buildNginxConfigWithSslIncludesRedirect() throws Exception { Method method = SshRsyncDeployer.class.getDeclaredMethod( @@ -76,4 +106,70 @@ class SshRsyncDeployerTest { assertFalse(config.contains("ssl_certificate")); assertFalse(config.contains("return 301")); } + + @Test + void buildNginxConfigWithoutSslHasNoSslRedirect() throws Exception { + Method method = SshRsyncDeployer.class.getDeclaredMethod( + "buildNginxConfig", String.class, String.class, boolean.class); + method.setAccessible(true); + + String config = (String) method.invoke(deployer, "example.com", "/var/www/public", false); + + assertFalse(config.contains("return 301")); + assertFalse(config.contains("ssl_certificate")); + assertFalse(config.contains("listen 443")); + } + + @Test + void provisionSslThrowsWhenSslEmailIsNull() throws Exception { + Method method = SshRsyncDeployer.class.getDeclaredMethod( + "provisionSsl", String.class, SelfHostedConfig.class, String.class); + method.setAccessible(true); + + SelfHostedConfig config = SelfHostedConfig.builder() + .domain("example.com") + .sslEmail(null) + .build(); + + var thrown = assertThrows(Exception.class, + () -> method.invoke(deployer, "/path/key", config, "/var/www/public")); + assertTrue(thrown.getCause().getMessage().contains("SSL email is required")); + } + + @Test + void writeSiteFilesCreatesDirectoryStructure() throws Exception { + Method method = SshRsyncDeployer.class.getDeclaredMethod( + "writeSiteFiles", Path.class, SiteOutput.class); + method.setAccessible(true); + + Path tempDir = Files.createTempDirectory("test-deploy"); + try { + Map pages = Map.of( + "home", "

Home

", + "about", "

About

" + ); + SiteOutput output = new SiteOutput("body {}", "console.log('hi')", pages); + + method.invoke(deployer, tempDir, output); + + Path siteDir = tempDir.resolve("site"); + assertTrue(Files.exists(siteDir.resolve("index.html"))); + assertTrue(Files.exists(siteDir.resolve("style.css"))); + assertTrue(Files.exists(siteDir.resolve("script.js"))); + assertTrue(Files.exists(siteDir.resolve("about/index.html"))); + assertTrue(Files.exists(siteDir.resolve("404.html"))); + assertEquals("

Home

", Files.readString(siteDir.resolve("index.html"))); + assertEquals("

About

", Files.readString(siteDir.resolve("about/index.html"))); + } finally { + try (var walk = Files.walk(tempDir)) { + walk.sorted(Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException e) { + } + }); + } + } + } }