add ssl certificates and tests
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
package com.krrishg.controller;
|
||||
|
||||
import com.krrishg.config.GlobalExceptionHandler;
|
||||
import com.krrishg.config.JwtAuthenticationToken;
|
||||
import com.krrishg.config.UserPrincipal;
|
||||
import com.krrishg.dto.SelfHostedConfigRequest;
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import com.krrishg.model.Site;
|
||||
import com.krrishg.repository.SelfHostedConfigRepository;
|
||||
import com.krrishg.repository.SiteRepository;
|
||||
import com.krrishg.service.Deployer;
|
||||
import com.krrishg.service.KeyManager;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
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(SelfHostedController.class)
|
||||
@Import({GlobalExceptionHandler.class, TestSecurityConfig.class})
|
||||
class SelfHostedControllerTest {
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private SiteRepository siteRepository;
|
||||
|
||||
@MockBean
|
||||
private SelfHostedConfigRepository selfHostedConfigRepository;
|
||||
|
||||
@MockBean
|
||||
private Deployer deployer;
|
||||
|
||||
@MockBean
|
||||
private KeyManager keyManager;
|
||||
|
||||
private UUID userId;
|
||||
private UUID siteId;
|
||||
private UserPrincipal principal;
|
||||
private JwtAuthenticationToken auth;
|
||||
|
||||
SelfHostedControllerTest(@Autowired MockMvc mockMvc, @Autowired ObjectMapper objectMapper) {
|
||||
this.mockMvc = mockMvc;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
siteId = UUID.randomUUID();
|
||||
userId = UUID.randomUUID();
|
||||
principal = new UserPrincipal(userId, "user@test.com", "Test User");
|
||||
auth = new JwtAuthenticationToken(principal, "test-token");
|
||||
}
|
||||
|
||||
void givenSaveReturnsInput() {
|
||||
when(selfHostedConfigRepository.save(any())).thenAnswer(invocation -> {
|
||||
SelfHostedConfig input = invocation.getArgument(0);
|
||||
if (input.getId() == null) {
|
||||
input.setId(UUID.randomUUID());
|
||||
}
|
||||
if (input.getCreatedAt() == null) {
|
||||
input.setCreatedAt(LocalDateTime.now());
|
||||
input.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
private Site givenSiteOwnedByUser() {
|
||||
Site site = Site.builder()
|
||||
.id(siteId)
|
||||
.userId(userId)
|
||||
.name("Test Site")
|
||||
.build();
|
||||
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
|
||||
return site;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getConfigReturnsConfigWithPublicKey() throws Exception {
|
||||
givenSiteOwnedByUser();
|
||||
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.siteId(siteId)
|
||||
.domain("example.com")
|
||||
.sshHost("192.168.1.1")
|
||||
.sshPort(22)
|
||||
.sshUser("deploy")
|
||||
.deployPath("/var/www/example/public")
|
||||
.nginxSitesPath("/etc/nginx/sites-available")
|
||||
.sslEnabled(true)
|
||||
.sslEmail("admin@example.com")
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
when(selfHostedConfigRepository.findBySiteId(siteId)).thenReturn(Optional.of(config));
|
||||
when(keyManager.getPublicKey(siteId.toString())).thenReturn("ssh-ed25519 AAA...");
|
||||
|
||||
mockMvc.perform(get("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.domain").value("example.com"))
|
||||
.andExpect(jsonPath("$.sshHost").value("192.168.1.1"))
|
||||
.andExpect(jsonPath("$.publicKey").value("ssh-ed25519 AAA..."))
|
||||
.andExpect(jsonPath("$.sslEnabled").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getConfigReturnsNotConfiguredWhenNoConfig() throws Exception {
|
||||
givenSiteOwnedByUser();
|
||||
when(selfHostedConfigRepository.findBySiteId(siteId)).thenReturn(Optional.empty());
|
||||
|
||||
mockMvc.perform(get("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.configured").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void putSaveConfigCreatesWithDefaults() throws Exception {
|
||||
givenSiteOwnedByUser();
|
||||
givenSaveReturnsInput();
|
||||
when(selfHostedConfigRepository.findBySiteId(siteId)).thenReturn(Optional.empty());
|
||||
when(keyManager.getPublicKey(siteId.toString())).thenReturn("ssh-ed25519 BBB...");
|
||||
|
||||
SelfHostedConfigRequest request = new SelfHostedConfigRequest();
|
||||
request.setDomain("mysite.com");
|
||||
request.setSshHost("10.0.0.1");
|
||||
request.setSshPort(2222);
|
||||
request.setSshUser("admin");
|
||||
request.setSslEnabled(true);
|
||||
request.setSslEmail("ssl@mysite.com");
|
||||
|
||||
mockMvc.perform(put("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.domain").value("mysite.com"))
|
||||
.andExpect(jsonPath("$.publicKey").value("ssh-ed25519 BBB..."));
|
||||
|
||||
verify(selfHostedConfigRepository).save(any(SelfHostedConfig.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void putSaveConfigUsesDefaultPathsWhenNotProvided() throws Exception {
|
||||
givenSiteOwnedByUser();
|
||||
givenSaveReturnsInput();
|
||||
when(selfHostedConfigRepository.findBySiteId(siteId)).thenReturn(Optional.empty());
|
||||
when(keyManager.getPublicKey(siteId.toString())).thenReturn("key");
|
||||
|
||||
SelfHostedConfigRequest request = new SelfHostedConfigRequest();
|
||||
request.setDomain("site.com");
|
||||
request.setSshHost("host");
|
||||
request.setSshPort(22);
|
||||
request.setSshUser("user");
|
||||
|
||||
mockMvc.perform(put("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(selfHostedConfigRepository).save(argThat(config ->
|
||||
"/var/www/{domain}/public".equals(config.getDeployPath()) &&
|
||||
"/etc/nginx/sites-available".equals(config.getNginxSitesPath())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void putReturns400OnValidationError() throws Exception {
|
||||
SelfHostedConfigRequest request = new SelfHostedConfigRequest();
|
||||
|
||||
mockMvc.perform(put("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRemovesConfigAndCallsDeployer() throws Exception {
|
||||
givenSiteOwnedByUser();
|
||||
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.siteId(siteId)
|
||||
.domain("example.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
when(selfHostedConfigRepository.findBySiteId(siteId)).thenReturn(Optional.of(config));
|
||||
|
||||
mockMvc.perform(delete("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.message").value("Self-hosted configuration removed"));
|
||||
|
||||
verify(deployer).remove(config);
|
||||
verify(selfHostedConfigRepository).delete(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSwallowsDeployerException() throws Exception {
|
||||
givenSiteOwnedByUser();
|
||||
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.siteId(siteId)
|
||||
.domain("example.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
when(selfHostedConfigRepository.findBySiteId(siteId)).thenReturn(Optional.of(config));
|
||||
doThrow(new RuntimeException("SSH error")).when(deployer).remove(config);
|
||||
|
||||
mockMvc.perform(delete("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(selfHostedConfigRepository).delete(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteHandlesNoConfig() throws Exception {
|
||||
givenSiteOwnedByUser();
|
||||
when(selfHostedConfigRepository.findBySiteId(siteId)).thenReturn(Optional.empty());
|
||||
|
||||
mockMvc.perform(delete("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(deployer, never()).remove(any());
|
||||
verify(selfHostedConfigRepository, never()).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getConfigReturns400WhenSiteNotFound() throws Exception {
|
||||
when(siteRepository.findById(siteId)).thenReturn(Optional.empty());
|
||||
|
||||
mockMvc.perform(get("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getConfigReturns400ForWrongOwner() throws Exception {
|
||||
Site site = Site.builder()
|
||||
.id(siteId)
|
||||
.userId(UUID.randomUUID())
|
||||
.name("Test Site")
|
||||
.build();
|
||||
when(siteRepository.findById(siteId)).thenReturn(Optional.of(site));
|
||||
|
||||
mockMvc.perform(get("/api/sites/{id}/self-hosted", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.krrishg.repository;
|
||||
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DataJpaTest
|
||||
class SelfHostedConfigRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private SelfHostedConfigRepository repository;
|
||||
|
||||
@Test
|
||||
void saveAndFindBySiteId() {
|
||||
UUID siteId = UUID.randomUUID();
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(siteId)
|
||||
.domain("example.com")
|
||||
.sshHost("192.168.1.1")
|
||||
.sshPort(22)
|
||||
.sshUser("deploy")
|
||||
.build();
|
||||
repository.save(config);
|
||||
|
||||
Optional<SelfHostedConfig> found = repository.findBySiteId(siteId);
|
||||
|
||||
assertTrue(found.isPresent());
|
||||
assertEquals("example.com", found.get().getDomain());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findBySiteIdReturnsEmptyWhenNotExists() {
|
||||
Optional<SelfHostedConfig> found = repository.findBySiteId(UUID.randomUUID());
|
||||
assertTrue(found.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByDomainReturnsConfig() {
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(UUID.randomUUID())
|
||||
.domain("mysite.com")
|
||||
.sshHost("10.0.0.1")
|
||||
.sshPort(2222)
|
||||
.sshUser("admin")
|
||||
.build();
|
||||
repository.save(config);
|
||||
|
||||
Optional<SelfHostedConfig> found = repository.findByDomain("mysite.com");
|
||||
|
||||
assertTrue(found.isPresent());
|
||||
assertEquals("mysite.com", found.get().getDomain());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByDomainReturnsEmptyWhenNotExists() {
|
||||
Optional<SelfHostedConfig> found = repository.findByDomain("nonexistent.com");
|
||||
assertTrue(found.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsBySiteIdReturnsTrueWhenExists() {
|
||||
UUID siteId = UUID.randomUUID();
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(siteId)
|
||||
.domain("exists.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
repository.save(config);
|
||||
|
||||
assertTrue(repository.existsBySiteId(siteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsBySiteIdReturnsFalseWhenNotExists() {
|
||||
assertFalse(repository.existsBySiteId(UUID.randomUUID()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsByDomainReturnsTrueWhenExists() {
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(UUID.randomUUID())
|
||||
.domain("domain-check.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
repository.save(config);
|
||||
|
||||
assertTrue(repository.existsByDomain("domain-check.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsByDomainReturnsFalseWhenNotExists() {
|
||||
assertFalse(repository.existsByDomain("no-domain.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void savedEntityHasIdAndTimestamps() {
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(UUID.randomUUID())
|
||||
.domain("timestamps.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
SelfHostedConfig saved = repository.save(config);
|
||||
|
||||
assertNotNull(saved.getId());
|
||||
assertNotNull(saved.getCreatedAt());
|
||||
assertNotNull(saved.getUpdatedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void siteIdIsUniqueByConstraint() {
|
||||
UUID siteId = UUID.randomUUID();
|
||||
|
||||
SelfHostedConfig config1 = SelfHostedConfig.builder()
|
||||
.siteId(siteId)
|
||||
.domain("first.com")
|
||||
.sshHost("host1")
|
||||
.sshPort(22)
|
||||
.sshUser("user1")
|
||||
.build();
|
||||
repository.saveAndFlush(config1);
|
||||
|
||||
SelfHostedConfig config2 = SelfHostedConfig.builder()
|
||||
.siteId(siteId)
|
||||
.domain("second.com")
|
||||
.sshHost("host2")
|
||||
.sshPort(22)
|
||||
.sshUser("user2")
|
||||
.build();
|
||||
|
||||
assertThrows(Exception.class, () -> repository.saveAndFlush(config2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.dto.SiteStructure;
|
||||
import com.krrishg.model.*;
|
||||
import com.krrishg.model.Site.DeploymentTarget;
|
||||
import com.krrishg.service.RenderService.SiteOutput;
|
||||
import com.krrishg.support.FakeGitRemoteRepository;
|
||||
import com.krrishg.support.FakeSelfHostedConfigRepository;
|
||||
import com.krrishg.support.FakeSiteRepository;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DeploymentServiceTest {
|
||||
|
||||
private FakeSiteRepository siteRepository;
|
||||
private FakeGitRemoteRepository gitRemoteRepository;
|
||||
private FakeSelfHostedConfigRepository selfHostedConfigRepository;
|
||||
|
||||
@Mock
|
||||
private RenderService renderService;
|
||||
|
||||
@Mock
|
||||
private GitService gitService;
|
||||
|
||||
@Mock
|
||||
private LLMService llmService;
|
||||
|
||||
@Mock
|
||||
private Deployer deployer;
|
||||
|
||||
private DeploymentService deploymentService;
|
||||
|
||||
private UUID siteId;
|
||||
private UUID userId;
|
||||
private Site site;
|
||||
private SiteStructure structure;
|
||||
private SiteOutput siteOutput;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
siteRepository = new FakeSiteRepository();
|
||||
gitRemoteRepository = new FakeGitRemoteRepository();
|
||||
selfHostedConfigRepository = new FakeSelfHostedConfigRepository();
|
||||
|
||||
deploymentService = new DeploymentService(renderService, gitService,
|
||||
siteRepository, llmService, gitRemoteRepository,
|
||||
selfHostedConfigRepository, deployer);
|
||||
|
||||
siteId = UUID.randomUUID();
|
||||
userId = UUID.randomUUID();
|
||||
|
||||
site = Site.builder()
|
||||
.userId(userId)
|
||||
.name("Test Site")
|
||||
.build();
|
||||
site.setId(siteId);
|
||||
site.setCreatedAt(LocalDateTime.now());
|
||||
site.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
structure = SiteStructure.builder()
|
||||
.pages(List.of(
|
||||
SiteStructure.PageStructure.builder()
|
||||
.title("Home").slug("home").rawHtml("<h1>Hello</h1>")
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
|
||||
siteOutput = new SiteOutput("css", "js", new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private void givenVersionExists() {
|
||||
GenerationVersion version = GenerationVersion.builder()
|
||||
.id("v1").siteId(siteId).versionNumber(1).fullStructure("{}").build();
|
||||
when(llmService.getVersions(siteId)).thenReturn(List.of(version));
|
||||
when(llmService.restoreVersion("v1")).thenReturn(structure);
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishWithGitPagesTargetDelegatesToGitService() {
|
||||
site.setDeploymentTarget(DeploymentTarget.GIT_PAGES);
|
||||
siteRepository.save(site);
|
||||
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.siteId(siteId)
|
||||
.provider(GitRemote.GitProvider.GITHUB)
|
||||
.remoteUrl("https://github.com/user/repo.git")
|
||||
.encryptedToken("token")
|
||||
.build();
|
||||
gitRemoteRepository.save(remote);
|
||||
|
||||
givenVersionExists();
|
||||
when(renderService.renderSite(structure)).thenReturn(siteOutput);
|
||||
when(gitService.deployToPages(eq(siteId.toString()), any(GitRemote.class), eq(structure)))
|
||||
.thenReturn("https://user.github.io/repo/");
|
||||
|
||||
String url = deploymentService.publish(siteId);
|
||||
|
||||
assertEquals("https://user.github.io/repo/", url);
|
||||
verify(gitService).deployToPages(siteId.toString(), remote, structure);
|
||||
Site updated = siteRepository.findById(siteId).orElseThrow();
|
||||
assertEquals(SiteStatus.PUBLISHED, updated.getStatus());
|
||||
assertNotNull(updated.getPublishedAt());
|
||||
assertEquals("https://user.github.io/repo/", updated.getPublishedUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishWithSelfHostedTargetDelegatesToDeployer() throws Exception {
|
||||
site.setDeploymentTarget(DeploymentTarget.SELF_HOSTED);
|
||||
siteRepository.save(site);
|
||||
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(siteId)
|
||||
.domain("example.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
selfHostedConfigRepository.save(config);
|
||||
|
||||
givenVersionExists();
|
||||
when(renderService.renderSite(structure)).thenReturn(siteOutput);
|
||||
when(deployer.deploy(any(SiteOutput.class), any(SelfHostedConfig.class)))
|
||||
.thenReturn("http://example.com/");
|
||||
when(deployer.buildUrl(any(SelfHostedConfig.class))).thenReturn("http://example.com/");
|
||||
|
||||
String url = deploymentService.publish(siteId);
|
||||
|
||||
assertEquals("http://example.com/", url);
|
||||
verify(deployer).deploy(siteOutput, config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishWithNoneTargetReturnsNullUrl() throws Exception {
|
||||
site.setDeploymentTarget(DeploymentTarget.NONE);
|
||||
siteRepository.save(site);
|
||||
|
||||
givenVersionExists();
|
||||
when(renderService.renderSite(structure)).thenReturn(siteOutput);
|
||||
|
||||
String url = deploymentService.publish(siteId);
|
||||
|
||||
assertNull(url);
|
||||
verify(gitService, never()).deployToPages(any(), any(), any());
|
||||
verify(deployer, never()).deploy(any(), any());
|
||||
Site updated = siteRepository.findById(siteId).orElseThrow();
|
||||
assertEquals(SiteStatus.PUBLISHED, updated.getStatus());
|
||||
assertNull(updated.getPublishedUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishThrowsWhenSiteNotFound() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> deploymentService.publish(UUID.randomUUID()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishThrowsWhenNoVersions() {
|
||||
siteRepository.save(site);
|
||||
when(llmService.getVersions(siteId)).thenReturn(List.of());
|
||||
|
||||
IllegalStateException ex = assertThrows(IllegalStateException.class,
|
||||
() -> deploymentService.publish(siteId));
|
||||
assertTrue(ex.getMessage().contains("No generation versions"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishWithGitPagesTargetThrowsWhenRemoteMissing() {
|
||||
site.setDeploymentTarget(DeploymentTarget.GIT_PAGES);
|
||||
siteRepository.save(site);
|
||||
|
||||
givenVersionExists();
|
||||
when(renderService.renderSite(structure)).thenReturn(siteOutput);
|
||||
|
||||
IllegalStateException ex = assertThrows(IllegalStateException.class,
|
||||
() -> deploymentService.publish(siteId));
|
||||
assertTrue(ex.getMessage().contains("no git remote is configured"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishWithSelfHostedTargetThrowsWhenConfigMissing() {
|
||||
site.setDeploymentTarget(DeploymentTarget.SELF_HOSTED);
|
||||
siteRepository.save(site);
|
||||
|
||||
givenVersionExists();
|
||||
when(renderService.renderSite(structure)).thenReturn(siteOutput);
|
||||
|
||||
IllegalStateException ex = assertThrows(IllegalStateException.class,
|
||||
() -> deploymentService.publish(siteId));
|
||||
assertTrue(ex.getMessage().contains("no self-hosted config exists"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpublishResetsSiteToDraft() {
|
||||
site.setStatus(SiteStatus.PUBLISHED);
|
||||
site.setPublishedUrl("https://example.com");
|
||||
site.setPublishedAt(LocalDateTime.now());
|
||||
siteRepository.save(site);
|
||||
|
||||
deploymentService.unpublish(siteId);
|
||||
|
||||
Site updated = siteRepository.findById(siteId).orElseThrow();
|
||||
assertEquals(SiteStatus.DRAFT, updated.getStatus());
|
||||
assertNull(updated.getPublishedAt());
|
||||
assertNull(updated.getPublishedUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpublishWithRemoteRemovesPages() {
|
||||
site.setDeploymentTarget(DeploymentTarget.GIT_PAGES);
|
||||
siteRepository.save(site);
|
||||
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.siteId(siteId)
|
||||
.provider(GitRemote.GitProvider.GITHUB)
|
||||
.remoteUrl("https://github.com/user/repo.git")
|
||||
.encryptedToken("token")
|
||||
.build();
|
||||
gitRemoteRepository.save(remote);
|
||||
|
||||
deploymentService.unpublish(siteId);
|
||||
|
||||
verify(gitService).removePages(siteId.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpublishWithSelfHostedConfigRemovesDeploy() throws Exception {
|
||||
siteRepository.save(site);
|
||||
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(siteId)
|
||||
.domain("example.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
selfHostedConfigRepository.save(config);
|
||||
|
||||
deploymentService.unpublish(siteId);
|
||||
|
||||
verify(deployer).remove(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpublishThrowsWhenSiteNotFound() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> deploymentService.unpublish(UUID.randomUUID()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpublishSwallowsDeployerException() throws Exception {
|
||||
siteRepository.save(site);
|
||||
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(siteId)
|
||||
.domain("example.com")
|
||||
.sshHost("host")
|
||||
.sshPort(22)
|
||||
.sshUser("user")
|
||||
.build();
|
||||
selfHostedConfigRepository.save(config);
|
||||
|
||||
doThrow(new RuntimeException("SSH failed")).when(deployer).remove(config);
|
||||
|
||||
deploymentService.unpublish(siteId);
|
||||
|
||||
Site updated = siteRepository.findById(siteId).orElseThrow();
|
||||
assertEquals(SiteStatus.DRAFT, updated.getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.GitRemote;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class GitHubPagesProviderTest {
|
||||
|
||||
private final GitHubPagesProvider provider = new GitHubPagesProvider();
|
||||
|
||||
@Test
|
||||
void branchNameIsGhPages() {
|
||||
assertEquals("gh-pages", provider.branchName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentRootIsTempDirItself(@TempDir Path tempDir) {
|
||||
assertEquals(tempDir, provider.contentRoot(tempDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeProviderFilesCreatesNoJekyll(@TempDir Path tempDir) throws Exception {
|
||||
provider.writeProviderFiles(tempDir);
|
||||
|
||||
Path nojekyll = tempDir.resolve(".nojekyll");
|
||||
assertTrue(Files.exists(nojekyll));
|
||||
assertEquals("", Files.readString(nojekyll));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlWithStandardRemote() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://github.com/user/my-repo.git")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://user.github.io/my-repo/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlWithoutGitSuffix() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://github.com/org/project")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://org.github.io/project/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlWithTrailingSlash() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://github.com/team/app/")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://team.github.io/app/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlReturnsOriginalWhenNotGithub() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://example.com/user/repo.git")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertNotNull(url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlHandlesSshUrl() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("git@github.com:user/project.git")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://user.github.io/project/", url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.GitRemote;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class GitLabPagesProviderTest {
|
||||
|
||||
private final GitLabPagesProvider provider = new GitLabPagesProvider();
|
||||
|
||||
@Test
|
||||
void branchNameIsGlPages() {
|
||||
assertEquals("gl-pages", provider.branchName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentRootIsPublicDirectory(@TempDir Path tempDir) {
|
||||
assertEquals(tempDir.resolve("public"), provider.contentRoot(tempDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeProviderFilesCreatesGitLabCiYml(@TempDir Path tempDir) throws Exception {
|
||||
provider.writeProviderFiles(tempDir);
|
||||
|
||||
Path ciFile = tempDir.resolve(".gitlab-ci.yml");
|
||||
assertTrue(Files.exists(ciFile));
|
||||
String content = Files.readString(ciFile);
|
||||
assertTrue(content.contains("pages:"));
|
||||
assertTrue(content.contains("gl-pages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlWithStandardRemote() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://gitlab.com/mygroup/myproject.git")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://mygroup.gitlab.io/myproject/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlWithoutGitSuffix() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://gitlab.com/team/app")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://team.gitlab.io/app/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlWithTrailingSlash() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://gitlab.com/org/repo/")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://org.gitlab.io/repo/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlReturnsOriginalUrlWhenNotGitlab() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("https://example.com/user/repo.git")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertNotNull(url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildPagesUrlHandlesSshUrl() {
|
||||
GitRemote remote = GitRemote.builder()
|
||||
.remoteUrl("git@gitlab.com:group/project.git")
|
||||
.build();
|
||||
|
||||
String url = provider.buildPagesUrl(remote);
|
||||
|
||||
assertEquals("https://group.gitlab.io/project/", url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.config.GitConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class KeyManagerTest {
|
||||
|
||||
private Path tempDir;
|
||||
private KeyManager keyManager;
|
||||
private String siteId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(@TempDir Path tempDir) {
|
||||
this.tempDir = tempDir;
|
||||
siteId = "test-site";
|
||||
|
||||
GitConfig gitConfig = new GitConfig() {
|
||||
@Override
|
||||
public Path getReposPath() {
|
||||
return KeyManagerTest.this.tempDir;
|
||||
}
|
||||
};
|
||||
gitConfig.setAuthorName("Test");
|
||||
gitConfig.setAuthorEmail("test@test.com");
|
||||
|
||||
keyManager = new KeyManager(gitConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasKeyPairReturnsFalseWhenKeysDoNotExist() {
|
||||
assertFalse(keyManager.hasKeyPair(siteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPublicKeyGeneratesKeysWhenMissing() {
|
||||
String publicKey = keyManager.getPublicKey(siteId);
|
||||
|
||||
assertNotNull(publicKey);
|
||||
assertTrue(publicKey.startsWith("ssh-ed25519"));
|
||||
assertTrue(keyManager.hasKeyPair(siteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPublicKeyReturnsExistingKey() {
|
||||
keyManager.generateKeyPair(siteId);
|
||||
String firstCall = keyManager.getPublicKey(siteId);
|
||||
String secondCall = keyManager.getPublicKey(siteId);
|
||||
|
||||
assertEquals(firstCall, secondCall);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateKeyPairCreatesBothFiles() {
|
||||
keyManager.generateKeyPair(siteId);
|
||||
|
||||
Path sshDir = tempDir.resolve(siteId).resolve(".ssh");
|
||||
assertTrue(Files.exists(sshDir.resolve("id_ed25519")));
|
||||
assertTrue(Files.exists(sshDir.resolve("id_ed25519.pub")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteKeysRemovesKeyFilesAndDirectory() {
|
||||
keyManager.generateKeyPair(siteId);
|
||||
assertTrue(keyManager.hasKeyPair(siteId));
|
||||
|
||||
keyManager.deleteKeys(siteId);
|
||||
|
||||
assertFalse(keyManager.hasKeyPair(siteId));
|
||||
assertFalse(Files.exists(tempDir.resolve(siteId).resolve(".ssh")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteKeysDoesNotThrowWhenKeysDoNotExist() {
|
||||
assertDoesNotThrow(() -> keyManager.deleteKeys(siteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrivateKeyPathGeneratesOnDemand() {
|
||||
Path keyPath = keyManager.getPrivateKeyPath(siteId);
|
||||
|
||||
assertTrue(Files.exists(keyPath));
|
||||
assertTrue(keyPath.endsWith("id_ed25519"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSshDirReturnsCorrectPath() {
|
||||
Path sshDir = keyManager.getSshDir(siteId);
|
||||
assertEquals(tempDir.resolve(siteId).resolve(".ssh"), sshDir);
|
||||
}
|
||||
}
|
||||
149
backend/src/test/java/com/krrishg/service/MediaServiceTest.java
Normal file
149
backend/src/test/java/com/krrishg/service/MediaServiceTest.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.service.MediaService.MediaUploadResult;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MediaServiceTest {
|
||||
|
||||
@Mock
|
||||
private S3Client s3Client;
|
||||
|
||||
private MediaService mediaService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
mediaService = new MediaService(s3Client);
|
||||
|
||||
var bucketField = MediaService.class.getDeclaredField("bucket");
|
||||
bucketField.setAccessible(true);
|
||||
bucketField.set(mediaService, "test-bucket");
|
||||
|
||||
var urlField = MediaService.class.getDeclaredField("publicUrlBase");
|
||||
urlField.setAccessible(true);
|
||||
urlField.set(mediaService, "https://cdn.example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadStoresFileAndReturnsResult() throws IOException {
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
when(file.isEmpty()).thenReturn(false);
|
||||
when(file.getSize()).thenReturn(1024L);
|
||||
when(file.getContentType()).thenReturn("image/jpeg");
|
||||
when(file.getOriginalFilename()).thenReturn("photo.jpg");
|
||||
when(file.getBytes()).thenReturn("image-data".getBytes());
|
||||
|
||||
UUID siteId = UUID.randomUUID();
|
||||
MediaUploadResult result = mediaService.upload(file, siteId);
|
||||
|
||||
assertNotNull(result.key());
|
||||
assertTrue(result.key().startsWith("media/" + siteId + "/"));
|
||||
assertTrue(result.key().endsWith(".jpg"));
|
||||
assertEquals("https://cdn.example.com/" + result.key(), result.url());
|
||||
assertEquals("photo.jpg", result.originalFilename());
|
||||
assertEquals("image/jpeg", result.contentType());
|
||||
assertEquals(1024L, result.size());
|
||||
|
||||
ArgumentCaptor<PutObjectRequest> requestCaptor = ArgumentCaptor.forClass(PutObjectRequest.class);
|
||||
verify(s3Client).putObject(requestCaptor.capture(), any(RequestBody.class));
|
||||
assertEquals("test-bucket", requestCaptor.getValue().bucket());
|
||||
assertEquals(result.key(), requestCaptor.getValue().key());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadRejectsEmptyFile() {
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
when(file.isEmpty()).thenReturn(true);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mediaService.upload(file, UUID.randomUUID()));
|
||||
verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadRejectsOversizedFile() {
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
when(file.isEmpty()).thenReturn(false);
|
||||
when(file.getSize()).thenReturn(11 * 1024 * 1024L);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mediaService.upload(file, UUID.randomUUID()));
|
||||
verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadRejectsUnsupportedContentType() {
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
when(file.isEmpty()).thenReturn(false);
|
||||
when(file.getSize()).thenReturn(1024L);
|
||||
when(file.getContentType()).thenReturn("application/exe");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mediaService.upload(file, UUID.randomUUID()));
|
||||
verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadRejectsNullContentType() {
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
when(file.isEmpty()).thenReturn(false);
|
||||
when(file.getSize()).thenReturn(1024L);
|
||||
when(file.getContentType()).thenReturn(null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mediaService.upload(file, UUID.randomUUID()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadHandlesFilenameWithoutExtension() throws IOException {
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
when(file.isEmpty()).thenReturn(false);
|
||||
when(file.getSize()).thenReturn(512L);
|
||||
when(file.getContentType()).thenReturn("image/png");
|
||||
when(file.getOriginalFilename()).thenReturn("logo");
|
||||
when(file.getBytes()).thenReturn("png-data".getBytes());
|
||||
|
||||
UUID siteId = UUID.randomUUID();
|
||||
MediaUploadResult result = mediaService.upload(file, siteId);
|
||||
|
||||
assertFalse(result.key().endsWith("."));
|
||||
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadThrowsOnIOException() throws IOException {
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
when(file.isEmpty()).thenReturn(false);
|
||||
when(file.getSize()).thenReturn(1024L);
|
||||
when(file.getContentType()).thenReturn("image/jpeg");
|
||||
when(file.getOriginalFilename()).thenReturn("img.jpg");
|
||||
when(file.getBytes()).thenThrow(new IOException("S3 unreachable"));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> mediaService.upload(file, UUID.randomUUID()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPublicUrlAppendsKey() {
|
||||
String url = mediaService.getPublicUrl("media/site/file.jpg");
|
||||
assertEquals("https://cdn.example.com/media/site/file.jpg", url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
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.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SshRsyncDeployerTest {
|
||||
|
||||
@Mock
|
||||
private KeyManager keyManager;
|
||||
|
||||
private SshRsyncDeployer deployer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
deployer = new SshRsyncDeployer(keyManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlReturnsHttpWhenSslDisabled() {
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.domain("example.com")
|
||||
.sslEnabled(false)
|
||||
.build();
|
||||
|
||||
String url = deployer.buildUrl(config);
|
||||
|
||||
assertEquals("http://example.com/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlReturnsHttpsWhenSslEnabled() {
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.domain("example.com")
|
||||
.sslEnabled(true)
|
||||
.build();
|
||||
|
||||
String url = deployer.buildUrl(config);
|
||||
|
||||
assertEquals("https://example.com/", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildNginxConfigWithSslIncludesRedirect() 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/example/public", true);
|
||||
|
||||
assertTrue(config.contains("listen 443 ssl http2"));
|
||||
assertTrue(config.contains("ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem"));
|
||||
assertTrue(config.contains("return 301 https://$host$request_uri"));
|
||||
assertTrue(config.contains("server_name example.com"));
|
||||
assertTrue(config.contains("listen 80;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildNginxConfigWithoutSslIsPlainHttp() throws Exception {
|
||||
Method method = SshRsyncDeployer.class.getDeclaredMethod(
|
||||
"buildNginxConfig", String.class, String.class, boolean.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
String config = (String) method.invoke(deployer, "mysite.org", "/var/www/mysite/public", false);
|
||||
|
||||
assertTrue(config.contains("listen 80;"));
|
||||
assertTrue(config.contains("root /var/www/mysite/public;"));
|
||||
assertFalse(config.contains("ssl_certificate"));
|
||||
assertFalse(config.contains("return 301"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.krrishg.service.llm;
|
||||
|
||||
import com.krrishg.dto.LLMOptions;
|
||||
import com.krrishg.dto.LLMResult;
|
||||
import com.krrishg.dto.Reference;
|
||||
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.http.HttpEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GeminiProviderTest {
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private GeminiProvider provider;
|
||||
|
||||
private LLMOptions defaultOptions;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestTemplateBuilder builder = new RestTemplateBuilder()
|
||||
.setConnectTimeout(Duration.ofSeconds(1));
|
||||
try {
|
||||
var field = RestTemplateBuilder.class.getDeclaredField("interceptors");
|
||||
field.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
provider = new GeminiProvider("test-key", "gemini-2.0-flash",
|
||||
"https://generativelanguage.googleapis.com/v1beta/models",
|
||||
builder);
|
||||
|
||||
try {
|
||||
var restTemplateField = GeminiProvider.class.getDeclaredField("restTemplate");
|
||||
restTemplateField.setAccessible(true);
|
||||
restTemplateField.set(provider, restTemplate);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
defaultOptions = LLMOptions.builder().build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateReturnsContentOnSuccess() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"candidates", List.of(
|
||||
Map.of("content", Map.of(
|
||||
"parts", List.of(Map.of("text", "Hello from Gemini"))
|
||||
))
|
||||
),
|
||||
"usageMetadata", Map.of(
|
||||
"promptTokenCount", 10,
|
||||
"candidatesTokenCount", 20
|
||||
)
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class),
|
||||
any(),
|
||||
any(HttpEntity.class),
|
||||
eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
LLMResult result = provider.generate("system", "user prompt", null, defaultOptions);
|
||||
|
||||
assertEquals("Hello from Gemini", result.getContent());
|
||||
assertEquals(10, result.getPromptTokens());
|
||||
assertEquals(20, result.getCompletionTokens());
|
||||
assertEquals("gemini-2.0-flash", result.getModel());
|
||||
assertTrue(result.getLatencyMs() >= 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateThrowsOnEmptyCandidates() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"candidates", List.of()
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> provider.generate("sys", "user", null, defaultOptions));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateThrowsOnNullResponseBody() {
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(null));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> provider.generate("sys", "user", null, defaultOptions));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateThrowsOnMalformedResponse() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"candidates", List.of(Map.of("invalid", "data"))
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> provider.generate("sys", "user", null, defaultOptions));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateIncludesImageReferencesInParts() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"candidates", List.of(
|
||||
Map.of("content", Map.of(
|
||||
"parts", List.of(Map.of("text", "result"))
|
||||
))
|
||||
),
|
||||
"usageMetadata", Map.of(
|
||||
"promptTokenCount", 5,
|
||||
"candidatesTokenCount", 10
|
||||
)
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
List<Reference> refs = List.of(
|
||||
Reference.builder()
|
||||
.type(Reference.ReferenceType.IMAGE)
|
||||
.url("data:image/png;base64,abc123")
|
||||
.altText("Screenshot")
|
||||
.build()
|
||||
);
|
||||
|
||||
LLMResult result = provider.generate("sys", "user", refs, defaultOptions);
|
||||
|
||||
assertEquals("result", result.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNameReturnsGemini() {
|
||||
assertEquals("gemini", provider.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateReturnsZeroTokensWhenUsageMissing() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"candidates", List.of(
|
||||
Map.of("content", Map.of(
|
||||
"parts", List.of(Map.of("text", "no usage"))
|
||||
))
|
||||
)
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
LLMResult result = provider.generate("sys", "prompt", null, defaultOptions);
|
||||
|
||||
assertEquals(0, result.getPromptTokens());
|
||||
assertEquals(0, result.getCompletionTokens());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.krrishg.service.llm;
|
||||
|
||||
import com.krrishg.dto.LLMOptions;
|
||||
import com.krrishg.dto.LLMResult;
|
||||
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.http.HttpEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OpenAIProviderTest {
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private OpenAIProvider provider;
|
||||
|
||||
private LLMOptions defaultOptions;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestTemplateBuilder builder = new RestTemplateBuilder()
|
||||
.setConnectTimeout(Duration.ofSeconds(1));
|
||||
|
||||
provider = new OpenAIProvider("sk-test", "gpt-4o-mini",
|
||||
"https://api.openai.com/v1/chat/completions", builder);
|
||||
|
||||
try {
|
||||
var field = OpenAIProvider.class.getDeclaredField("restTemplate");
|
||||
field.setAccessible(true);
|
||||
field.set(provider, restTemplate);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
defaultOptions = LLMOptions.builder().build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateReturnsContentOnSuccess() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"choices", List.of(
|
||||
Map.of("message", Map.of("content", "Hello from OpenAI"))
|
||||
),
|
||||
"usage", Map.of(
|
||||
"prompt_tokens", 15,
|
||||
"completion_tokens", 25
|
||||
)
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
LLMResult result = provider.generate("system", "user prompt", null, defaultOptions);
|
||||
|
||||
assertEquals("Hello from OpenAI", result.getContent());
|
||||
assertEquals(15, result.getPromptTokens());
|
||||
assertEquals(25, result.getCompletionTokens());
|
||||
assertEquals("gpt-4o-mini", result.getModel());
|
||||
assertTrue(result.getLatencyMs() >= 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateThrowsOnEmptyChoices() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"choices", List.of()
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> provider.generate("sys", "user", null, defaultOptions));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateThrowsOnNullResponseBody() {
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(null));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> provider.generate("sys", "user", null, defaultOptions));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateThrowsOnMalformedResponse() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"choices", List.of(Map.of("invalid", "data"))
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> provider.generate("sys", "user", null, defaultOptions));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNameReturnsOpenai() {
|
||||
assertEquals("openai", provider.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateReturnsZeroTokensWhenUsageMissing() {
|
||||
Map<String, Object> responseBody = Map.of(
|
||||
"choices", List.of(
|
||||
Map.of("message", Map.of("content", "no usage"))
|
||||
)
|
||||
);
|
||||
when(restTemplate.exchange(
|
||||
any(String.class), any(), any(HttpEntity.class), eq(Map.class)
|
||||
)).thenReturn(ResponseEntity.ok(responseBody));
|
||||
|
||||
LLMResult result = provider.generate("sys", "prompt", null, defaultOptions);
|
||||
|
||||
assertEquals(0, result.getPromptTokens());
|
||||
assertEquals(0, result.getCompletionTokens());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.krrishg.support;
|
||||
|
||||
import com.krrishg.model.GitRemote;
|
||||
import com.krrishg.repository.GitRemoteRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class FakeGitRemoteRepository
|
||||
extends InMemoryRepository<GitRemote, UUID>
|
||||
implements GitRemoteRepository {
|
||||
|
||||
@Override
|
||||
protected UUID getId(GitRemote entity) {
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GitRemote setId(GitRemote entity, UUID id) {
|
||||
entity.setId(id);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UUID generateId() {
|
||||
return UUID.randomUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GitRemote> findBySiteId(UUID siteId) {
|
||||
return store.values().stream()
|
||||
.filter(r -> siteId.equals(r.getSiteId()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsBySiteId(UUID siteId) {
|
||||
return store.values().stream()
|
||||
.anyMatch(r -> siteId.equals(r.getSiteId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBySiteId(UUID siteId) {
|
||||
store.values().removeIf(r -> siteId.equals(r.getSiteId()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user