add tests
This commit is contained in:
193
backend/src/test/java/com/krrishg/config/JwtConfigTest.java
Normal file
193
backend/src/test/java/com/krrishg/config/JwtConfigTest.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
backend/src/test/java/com/krrishg/config/SecurityConfigTest.java
Normal file
144
backend/src/test/java/com/krrishg/config/SecurityConfigTest.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
package com.krrishg.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.krrishg.model.SelfHostedConfig;
|
import com.krrishg.model.SelfHostedConfig;
|
||||||
|
import com.krrishg.service.RenderService.SiteOutput;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
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.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@@ -48,6 +54,30 @@ class SshRsyncDeployerTest {
|
|||||||
assertEquals("https://example.com/", url);
|
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
|
@Test
|
||||||
void buildNginxConfigWithSslIncludesRedirect() throws Exception {
|
void buildNginxConfigWithSslIncludesRedirect() throws Exception {
|
||||||
Method method = SshRsyncDeployer.class.getDeclaredMethod(
|
Method method = SshRsyncDeployer.class.getDeclaredMethod(
|
||||||
@@ -76,4 +106,70 @@ class SshRsyncDeployerTest {
|
|||||||
assertFalse(config.contains("ssl_certificate"));
|
assertFalse(config.contains("ssl_certificate"));
|
||||||
assertFalse(config.contains("return 301"));
|
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<String, String> pages = Map.of(
|
||||||
|
"home", "<h1>Home</h1>",
|
||||||
|
"about", "<h1>About</h1>"
|
||||||
|
);
|
||||||
|
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("<h1>Home</h1>", Files.readString(siteDir.resolve("index.html")));
|
||||||
|
assertEquals("<h1>About</h1>", 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) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user