feat: remove migrated environment/app/deployment/runtime code from SaaS
- Delete environment/, app/, deployment/, runtime/ packages (source + tests) - Delete apikey/ package (tied to environments, table will be dropped) - Strip AsyncConfig to empty @EnableAsync (no more deploymentExecutor bean) - Remove EnvironmentService dependency from TenantService - Remove environment/app isolation from TenantIsolationInterceptor - Remove environment seeding from BootstrapDataSeeder - Refactor ServerApiClient to use LogtoConfig instead of RuntimeConfig - Add server-endpoint property to LogtoConfig (was in RuntimeConfig) - Remove runtime config section and multipart config from application.yml Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.apikey;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ApiKeyServiceTest {
|
||||
|
||||
@Test
|
||||
void generatedKeyShouldHaveCmkPrefix() {
|
||||
var service = new ApiKeyService(null);
|
||||
var key = service.generate();
|
||||
assertThat(key.plaintext()).startsWith("cmk_");
|
||||
assertThat(key.prefix()).hasSize(12);
|
||||
assertThat(key.keyHash()).hasSize(64);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedKeyHashShouldBeConsistent() {
|
||||
var service = new ApiKeyService(null);
|
||||
var key = service.generate();
|
||||
String rehash = ApiKeyService.sha256Hex(key.plaintext());
|
||||
assertThat(rehash).isEqualTo(key.keyHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
void twoGeneratedKeysShouldDiffer() {
|
||||
var service = new ApiKeyService(null);
|
||||
var key1 = service.generate();
|
||||
var key2 = service.generate();
|
||||
assertThat(key1.plaintext()).isNotEqualTo(key2.plaintext());
|
||||
assertThat(key1.keyHash()).isNotEqualTo(key2.keyHash());
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.app;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.siegeln.cameleer.saas.TestcontainersConfig;
|
||||
import net.siegeln.cameleer.saas.TestSecurityConfig;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
|
||||
import net.siegeln.cameleer.saas.license.LicenseDefaults;
|
||||
import net.siegeln.cameleer.saas.license.LicenseEntity;
|
||||
import net.siegeln.cameleer.saas.license.LicenseRepository;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantRepository;
|
||||
import net.siegeln.cameleer.saas.tenant.Tier;
|
||||
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.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Import({TestcontainersConfig.class, TestSecurityConfig.class})
|
||||
@ActiveProfiles("test")
|
||||
class AppControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private AppRepository appRepository;
|
||||
|
||||
@Autowired
|
||||
private EnvironmentRepository environmentRepository;
|
||||
|
||||
@Autowired
|
||||
private LicenseRepository licenseRepository;
|
||||
|
||||
@Autowired
|
||||
private TenantRepository tenantRepository;
|
||||
|
||||
private UUID environmentId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
appRepository.deleteAll();
|
||||
environmentRepository.deleteAll();
|
||||
licenseRepository.deleteAll();
|
||||
tenantRepository.deleteAll();
|
||||
|
||||
var tenant = new TenantEntity();
|
||||
tenant.setName("Test Org");
|
||||
tenant.setSlug("test-org-" + System.nanoTime());
|
||||
var savedTenant = tenantRepository.save(tenant);
|
||||
var tenantId = savedTenant.getId();
|
||||
|
||||
var license = new LicenseEntity();
|
||||
license.setTenantId(tenantId);
|
||||
license.setTier("MID");
|
||||
license.setFeatures(LicenseDefaults.featuresForTier(Tier.MID));
|
||||
license.setLimits(LicenseDefaults.limitsForTier(Tier.MID));
|
||||
license.setExpiresAt(Instant.now().plus(365, ChronoUnit.DAYS));
|
||||
license.setToken("test-token");
|
||||
licenseRepository.save(license);
|
||||
|
||||
var env = new net.siegeln.cameleer.saas.environment.EnvironmentEntity();
|
||||
env.setTenantId(tenantId);
|
||||
env.setSlug("default");
|
||||
env.setDisplayName("Default");
|
||||
var savedEnv = environmentRepository.save(env);
|
||||
environmentId = savedEnv.getId();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createApp_shouldReturn201() throws Exception {
|
||||
var metadata = new MockMultipartFile("metadata", "", "application/json",
|
||||
"""
|
||||
{"slug": "order-svc", "displayName": "Order Service"}
|
||||
""".getBytes());
|
||||
var jar = new MockMultipartFile("file", "order-service.jar",
|
||||
"application/java-archive", "fake-jar".getBytes());
|
||||
|
||||
mockMvc.perform(multipart("/api/environments/" + environmentId + "/apps")
|
||||
.file(jar)
|
||||
.file(metadata)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"))))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.slug").value("order-svc"))
|
||||
.andExpect(jsonPath("$.displayName").value("Order Service"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createApp_nonJarFile_shouldReturn400() throws Exception {
|
||||
var metadata = new MockMultipartFile("metadata", "", "application/json",
|
||||
"""
|
||||
{"slug": "order-svc", "displayName": "Order Service"}
|
||||
""".getBytes());
|
||||
var txt = new MockMultipartFile("file", "readme.txt",
|
||||
"text/plain", "hello".getBytes());
|
||||
|
||||
mockMvc.perform(multipart("/api/environments/" + environmentId + "/apps")
|
||||
.file(txt)
|
||||
.file(metadata)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"))))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listApps_shouldReturnAll() throws Exception {
|
||||
var metadata = new MockMultipartFile("metadata", "", "application/json",
|
||||
"""
|
||||
{"slug": "billing-svc", "displayName": "Billing Service"}
|
||||
""".getBytes());
|
||||
var jar = new MockMultipartFile("file", "billing-service.jar",
|
||||
"application/java-archive", "fake-jar".getBytes());
|
||||
|
||||
mockMvc.perform(multipart("/api/environments/" + environmentId + "/apps")
|
||||
.file(jar)
|
||||
.file(metadata)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"))))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(get("/api/environments/" + environmentId + "/apps")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].slug").value("billing-svc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteApp_shouldReturn204() throws Exception {
|
||||
var metadata = new MockMultipartFile("metadata", "", "application/json",
|
||||
"""
|
||||
{"slug": "payment-svc", "displayName": "Payment Service"}
|
||||
""".getBytes());
|
||||
var jar = new MockMultipartFile("file", "payment-service.jar",
|
||||
"application/java-archive", "fake-jar".getBytes());
|
||||
|
||||
var createResult = mockMvc.perform(multipart("/api/environments/" + environmentId + "/apps")
|
||||
.file(jar)
|
||||
.file(metadata)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"))))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
String appId = objectMapper.readTree(createResult.getResponse().getContentAsString())
|
||||
.get("id").asText();
|
||||
|
||||
mockMvc.perform(delete("/api/environments/" + environmentId + "/apps/" + appId)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"))))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.app;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
|
||||
import net.siegeln.cameleer.saas.license.LicenseEntity;
|
||||
import net.siegeln.cameleer.saas.license.LicenseRepository;
|
||||
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class AppServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Mock
|
||||
private AppRepository appRepository;
|
||||
|
||||
@Mock
|
||||
private EnvironmentRepository environmentRepository;
|
||||
|
||||
@Mock
|
||||
private LicenseRepository licenseRepository;
|
||||
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@Mock
|
||||
private RuntimeConfig runtimeConfig;
|
||||
|
||||
private AppService appService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(runtimeConfig.getJarStoragePath()).thenReturn(tempDir.toString());
|
||||
when(runtimeConfig.getMaxJarSize()).thenReturn(209715200L);
|
||||
appService = new AppService(appRepository, environmentRepository, licenseRepository, auditService, runtimeConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_shouldStoreJarAndCreateApp() throws Exception {
|
||||
var envId = UUID.randomUUID();
|
||||
var tenantId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
|
||||
var env = new EnvironmentEntity();
|
||||
env.setId(envId);
|
||||
env.setTenantId(tenantId);
|
||||
env.setSlug("default");
|
||||
|
||||
var license = new LicenseEntity();
|
||||
license.setTenantId(tenantId);
|
||||
license.setTier("MID");
|
||||
|
||||
var jarBytes = "fake-jar-content".getBytes();
|
||||
var jarFile = new MockMultipartFile("file", "myapp.jar", "application/java-archive", jarBytes);
|
||||
|
||||
when(environmentRepository.findById(envId)).thenReturn(Optional.of(env));
|
||||
when(appRepository.existsByEnvironmentIdAndSlug(envId, "myapp")).thenReturn(false);
|
||||
when(appRepository.countByTenantId(tenantId)).thenReturn(0L);
|
||||
when(licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId))
|
||||
.thenReturn(Optional.of(license));
|
||||
when(appRepository.save(any(AppEntity.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = appService.create(envId, "myapp", "My App", jarFile, null, null, actorId);
|
||||
|
||||
assertThat(result.getSlug()).isEqualTo("myapp");
|
||||
assertThat(result.getDisplayName()).isEqualTo("My App");
|
||||
assertThat(result.getEnvironmentId()).isEqualTo(envId);
|
||||
assertThat(result.getJarOriginalFilename()).isEqualTo("myapp.jar");
|
||||
assertThat(result.getJarSizeBytes()).isEqualTo((long) jarBytes.length);
|
||||
assertThat(result.getJarChecksum()).isNotBlank();
|
||||
assertThat(result.getJarStoragePath()).contains("tenants")
|
||||
.contains("envs")
|
||||
.contains("apps")
|
||||
.endsWith("app.jar");
|
||||
|
||||
var actionCaptor = ArgumentCaptor.forClass(AuditAction.class);
|
||||
verify(auditService).log(any(), any(), any(), actionCaptor.capture(), any(), any(), any(), any(), any());
|
||||
assertThat(actionCaptor.getValue()).isEqualTo(AuditAction.APP_CREATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_shouldRejectNonJarFile() {
|
||||
var envId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
|
||||
var textFile = new MockMultipartFile("file", "readme.txt", "text/plain", "hello".getBytes());
|
||||
|
||||
assertThatThrownBy(() -> appService.create(envId, "myapp", "My App", textFile, null, null, actorId))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining(".jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_shouldRejectDuplicateSlug() {
|
||||
var envId = UUID.randomUUID();
|
||||
var tenantId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
|
||||
var env = new EnvironmentEntity();
|
||||
env.setId(envId);
|
||||
env.setTenantId(tenantId);
|
||||
env.setSlug("default");
|
||||
|
||||
var jarFile = new MockMultipartFile("file", "myapp.jar", "application/java-archive", "fake-jar".getBytes());
|
||||
|
||||
when(environmentRepository.findById(envId)).thenReturn(Optional.of(env));
|
||||
when(appRepository.existsByEnvironmentIdAndSlug(envId, "myapp")).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> appService.create(envId, "myapp", "My App", jarFile, null, null, actorId))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("myapp");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reuploadJar_shouldUpdateChecksumAndPath() throws Exception {
|
||||
var appId = UUID.randomUUID();
|
||||
var envId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
|
||||
var existingApp = new AppEntity();
|
||||
existingApp.setId(appId);
|
||||
existingApp.setEnvironmentId(envId);
|
||||
existingApp.setSlug("myapp");
|
||||
existingApp.setDisplayName("My App");
|
||||
existingApp.setJarStoragePath("tenants/some-tenant/envs/default/apps/myapp/app.jar");
|
||||
existingApp.setJarChecksum("oldchecksum");
|
||||
existingApp.setJarOriginalFilename("old.jar");
|
||||
existingApp.setJarSizeBytes(100L);
|
||||
|
||||
var newJarBytes = "new-jar-content".getBytes();
|
||||
var newJarFile = new MockMultipartFile("file", "new-myapp.jar", "application/java-archive", newJarBytes);
|
||||
|
||||
when(appRepository.findById(appId)).thenReturn(Optional.of(existingApp));
|
||||
when(appRepository.save(any(AppEntity.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = appService.reuploadJar(appId, newJarFile, actorId);
|
||||
|
||||
assertThat(result.getJarOriginalFilename()).isEqualTo("new-myapp.jar");
|
||||
assertThat(result.getJarSizeBytes()).isEqualTo((long) newJarBytes.length);
|
||||
assertThat(result.getJarChecksum()).isNotBlank();
|
||||
assertThat(result.getJarChecksum()).isNotEqualTo("oldchecksum");
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.deployment;
|
||||
|
||||
import net.siegeln.cameleer.saas.TestSecurityConfig;
|
||||
import net.siegeln.cameleer.saas.TestcontainersConfig;
|
||||
import net.siegeln.cameleer.saas.app.AppEntity;
|
||||
import net.siegeln.cameleer.saas.app.AppRepository;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
|
||||
import net.siegeln.cameleer.saas.license.LicenseDefaults;
|
||||
import net.siegeln.cameleer.saas.license.LicenseEntity;
|
||||
import net.siegeln.cameleer.saas.license.LicenseRepository;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantRepository;
|
||||
import net.siegeln.cameleer.saas.tenant.Tier;
|
||||
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.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Import({TestcontainersConfig.class, TestSecurityConfig.class})
|
||||
@ActiveProfiles("test")
|
||||
class DeploymentControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private DeploymentRepository deploymentRepository;
|
||||
|
||||
@Autowired
|
||||
private AppRepository appRepository;
|
||||
|
||||
@Autowired
|
||||
private EnvironmentRepository environmentRepository;
|
||||
|
||||
@Autowired
|
||||
private LicenseRepository licenseRepository;
|
||||
|
||||
@Autowired
|
||||
private TenantRepository tenantRepository;
|
||||
|
||||
private UUID appId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
deploymentRepository.deleteAll();
|
||||
appRepository.deleteAll();
|
||||
environmentRepository.deleteAll();
|
||||
licenseRepository.deleteAll();
|
||||
tenantRepository.deleteAll();
|
||||
|
||||
var tenant = new TenantEntity();
|
||||
tenant.setName("Test Org");
|
||||
tenant.setSlug("test-org-" + System.nanoTime());
|
||||
var savedTenant = tenantRepository.save(tenant);
|
||||
var tenantId = savedTenant.getId();
|
||||
|
||||
var license = new LicenseEntity();
|
||||
license.setTenantId(tenantId);
|
||||
license.setTier("MID");
|
||||
license.setFeatures(LicenseDefaults.featuresForTier(Tier.MID));
|
||||
license.setLimits(LicenseDefaults.limitsForTier(Tier.MID));
|
||||
license.setExpiresAt(Instant.now().plus(365, ChronoUnit.DAYS));
|
||||
license.setToken("test-token");
|
||||
licenseRepository.save(license);
|
||||
|
||||
var env = new EnvironmentEntity();
|
||||
env.setTenantId(tenantId);
|
||||
env.setSlug("default");
|
||||
env.setDisplayName("Default");
|
||||
var savedEnv = environmentRepository.save(env);
|
||||
|
||||
var app = new AppEntity();
|
||||
app.setEnvironmentId(savedEnv.getId());
|
||||
app.setSlug("test-app");
|
||||
app.setDisplayName("Test App");
|
||||
app.setJarStoragePath("tenants/test-org/envs/default/apps/test-app/app.jar");
|
||||
app.setJarChecksum("abc123def456");
|
||||
var savedApp = appRepository.save(app);
|
||||
appId = savedApp.getId();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listDeployments_shouldReturnEmpty() throws Exception {
|
||||
mockMvc.perform(get("/api/apps/" + appId + "/deployments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.andExpect(jsonPath("$.length()").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDeployment_notFound_shouldReturn404() throws Exception {
|
||||
mockMvc.perform(get("/api/apps/" + appId + "/deployments/" + UUID.randomUUID())
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deploy_noAuth_shouldReturn401() throws Exception {
|
||||
mockMvc.perform(post("/api/apps/" + appId + "/deploy"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.deployment;
|
||||
|
||||
import net.siegeln.cameleer.saas.app.AppEntity;
|
||||
import net.siegeln.cameleer.saas.app.AppRepository;
|
||||
import net.siegeln.cameleer.saas.app.AppService;
|
||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
|
||||
import net.siegeln.cameleer.saas.runtime.BuildImageRequest;
|
||||
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
|
||||
import net.siegeln.cameleer.saas.runtime.RuntimeOrchestrator;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantRepository;
|
||||
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.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class DeploymentServiceTest {
|
||||
|
||||
@Mock
|
||||
private DeploymentRepository deploymentRepository;
|
||||
|
||||
@Mock
|
||||
private AppRepository appRepository;
|
||||
|
||||
@Mock
|
||||
private AppService appService;
|
||||
|
||||
@Mock
|
||||
private EnvironmentRepository environmentRepository;
|
||||
|
||||
@Mock
|
||||
private TenantRepository tenantRepository;
|
||||
|
||||
@Mock
|
||||
private RuntimeOrchestrator runtimeOrchestrator;
|
||||
|
||||
@Mock
|
||||
private RuntimeConfig runtimeConfig;
|
||||
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@Mock
|
||||
private DeploymentExecutor deploymentExecutor;
|
||||
|
||||
private DeploymentService deploymentService;
|
||||
|
||||
private UUID appId;
|
||||
private UUID envId;
|
||||
private UUID tenantId;
|
||||
private UUID actorId;
|
||||
private AppEntity app;
|
||||
private EnvironmentEntity env;
|
||||
private TenantEntity tenant;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
deploymentService = new DeploymentService(
|
||||
deploymentRepository,
|
||||
appRepository,
|
||||
environmentRepository,
|
||||
runtimeOrchestrator,
|
||||
auditService,
|
||||
deploymentExecutor
|
||||
);
|
||||
|
||||
appId = UUID.randomUUID();
|
||||
envId = UUID.randomUUID();
|
||||
tenantId = UUID.randomUUID();
|
||||
actorId = UUID.randomUUID();
|
||||
|
||||
env = new EnvironmentEntity();
|
||||
env.setId(envId);
|
||||
env.setTenantId(tenantId);
|
||||
env.setSlug("prod");
|
||||
|
||||
tenant = new TenantEntity();
|
||||
tenant.setSlug("acme");
|
||||
|
||||
app = new AppEntity();
|
||||
app.setId(appId);
|
||||
app.setEnvironmentId(envId);
|
||||
app.setSlug("myapp");
|
||||
app.setDisplayName("My App");
|
||||
app.setJarStoragePath("tenants/acme/envs/prod/apps/myapp/app.jar");
|
||||
|
||||
when(runtimeConfig.getBaseImage()).thenReturn("cameleer-runtime-base:latest");
|
||||
when(runtimeConfig.getDockerNetwork()).thenReturn("cameleer");
|
||||
when(runtimeConfig.getAgentHealthPort()).thenReturn(9464);
|
||||
when(runtimeConfig.getHealthCheckTimeout()).thenReturn(60);
|
||||
when(runtimeConfig.parseMemoryLimitBytes()).thenReturn(536870912L);
|
||||
when(runtimeConfig.getContainerCpuShares()).thenReturn(512);
|
||||
when(runtimeConfig.getCameleer3ServerEndpoint()).thenReturn("http://cameleer3-server:8081");
|
||||
|
||||
when(appRepository.findById(appId)).thenReturn(Optional.of(app));
|
||||
when(environmentRepository.findById(envId)).thenReturn(Optional.of(env));
|
||||
when(tenantRepository.findById(tenantId)).thenReturn(Optional.of(tenant));
|
||||
when(deploymentRepository.findMaxVersionByAppId(appId)).thenReturn(0);
|
||||
when(deploymentRepository.save(any(DeploymentEntity.class))).thenAnswer(inv -> {
|
||||
var d = (DeploymentEntity) inv.getArgument(0);
|
||||
if (d.getId() == null) {
|
||||
d.setId(UUID.randomUUID());
|
||||
}
|
||||
return d;
|
||||
});
|
||||
when(appService.resolveJarPath(any())).thenReturn(Path.of("/data/jars/tenants/acme/envs/prod/apps/myapp/app.jar"));
|
||||
when(runtimeOrchestrator.buildImage(any(BuildImageRequest.class))).thenReturn("sha256:abc123");
|
||||
when(runtimeOrchestrator.startContainer(any())).thenReturn("container-id-123");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deploy_shouldCreateDeploymentWithBuildingStatus() {
|
||||
var result = deploymentService.deploy(appId, actorId);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getAppId()).isEqualTo(appId);
|
||||
assertThat(result.getVersion()).isEqualTo(1);
|
||||
assertThat(result.getObservedStatus()).isEqualTo(ObservedStatus.BUILDING);
|
||||
assertThat(result.getImageRef()).contains("myapp").contains("v1");
|
||||
|
||||
var actionCaptor = ArgumentCaptor.forClass(AuditAction.class);
|
||||
verify(auditService).log(any(), any(), any(), actionCaptor.capture(), any(), any(), any(), any(), any());
|
||||
assertThat(actionCaptor.getValue()).isEqualTo(AuditAction.APP_DEPLOY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deploy_shouldRejectAppWithNoJar() {
|
||||
app.setJarStoragePath(null);
|
||||
|
||||
assertThatThrownBy(() -> deploymentService.deploy(appId, actorId))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("JAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop_shouldUpdateDesiredStatus() {
|
||||
var deploymentId = UUID.randomUUID();
|
||||
app.setCurrentDeploymentId(deploymentId);
|
||||
|
||||
var deployment = new DeploymentEntity();
|
||||
deployment.setId(deploymentId);
|
||||
deployment.setAppId(appId);
|
||||
deployment.setVersion(1);
|
||||
deployment.setImageRef("cameleer-runtime-prod-myapp:v1");
|
||||
deployment.setObservedStatus(ObservedStatus.RUNNING);
|
||||
deployment.setOrchestratorMetadata(Map.of("containerId", "container-id-123"));
|
||||
|
||||
when(deploymentRepository.findById(deploymentId)).thenReturn(Optional.of(deployment));
|
||||
|
||||
var result = deploymentService.stop(appId, actorId);
|
||||
|
||||
assertThat(result.getDesiredStatus()).isEqualTo(DesiredStatus.STOPPED);
|
||||
assertThat(result.getObservedStatus()).isEqualTo(ObservedStatus.STOPPED);
|
||||
|
||||
var actionCaptor = ArgumentCaptor.forClass(AuditAction.class);
|
||||
verify(auditService).log(any(), any(), any(), actionCaptor.capture(), any(), any(), any(), any(), any());
|
||||
assertThat(actionCaptor.getValue()).isEqualTo(AuditAction.APP_STOP);
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.environment;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.siegeln.cameleer.saas.TestcontainersConfig;
|
||||
import net.siegeln.cameleer.saas.TestSecurityConfig;
|
||||
import net.siegeln.cameleer.saas.environment.dto.CreateEnvironmentRequest;
|
||||
import net.siegeln.cameleer.saas.environment.dto.UpdateEnvironmentRequest;
|
||||
import net.siegeln.cameleer.saas.license.LicenseDefaults;
|
||||
import net.siegeln.cameleer.saas.license.LicenseEntity;
|
||||
import net.siegeln.cameleer.saas.license.LicenseRepository;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantRepository;
|
||||
import net.siegeln.cameleer.saas.tenant.Tier;
|
||||
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.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Import({TestcontainersConfig.class, TestSecurityConfig.class})
|
||||
@ActiveProfiles("test")
|
||||
class EnvironmentControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private EnvironmentRepository environmentRepository;
|
||||
|
||||
@Autowired
|
||||
private LicenseRepository licenseRepository;
|
||||
|
||||
@Autowired
|
||||
private TenantRepository tenantRepository;
|
||||
|
||||
private UUID tenantId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
environmentRepository.deleteAll();
|
||||
licenseRepository.deleteAll();
|
||||
tenantRepository.deleteAll();
|
||||
|
||||
var tenant = new TenantEntity();
|
||||
tenant.setName("Test Org");
|
||||
tenant.setSlug("test-org-" + System.nanoTime());
|
||||
var savedTenant = tenantRepository.save(tenant);
|
||||
tenantId = savedTenant.getId();
|
||||
|
||||
var license = new LicenseEntity();
|
||||
license.setTenantId(tenantId);
|
||||
license.setTier("MID");
|
||||
license.setFeatures(LicenseDefaults.featuresForTier(Tier.MID));
|
||||
license.setLimits(LicenseDefaults.limitsForTier(Tier.MID));
|
||||
license.setExpiresAt(Instant.now().plus(365, ChronoUnit.DAYS));
|
||||
license.setToken("test-token");
|
||||
licenseRepository.save(license);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createEnvironment_shouldReturn201() throws Exception {
|
||||
var request = new CreateEnvironmentRequest("prod", "Production");
|
||||
|
||||
mockMvc.perform(post("/api/tenants/" + tenantId + "/environments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.slug").value("prod"))
|
||||
.andExpect(jsonPath("$.displayName").value("Production"))
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createEnvironment_duplicateSlug_shouldReturn409() throws Exception {
|
||||
var request = new CreateEnvironmentRequest("staging", "Staging");
|
||||
|
||||
mockMvc.perform(post("/api/tenants/" + tenantId + "/environments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(post("/api/tenants/" + tenantId + "/environments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listEnvironments_shouldReturnAll() throws Exception {
|
||||
var request = new CreateEnvironmentRequest("dev", "Development");
|
||||
|
||||
mockMvc.perform(post("/api/tenants/" + tenantId + "/environments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(get("/api/tenants/" + tenantId + "/environments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_platform:admin"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].slug").value("dev"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateEnvironment_shouldReturn200() throws Exception {
|
||||
var createRequest = new CreateEnvironmentRequest("qa", "QA");
|
||||
|
||||
var createResult = mockMvc.perform(post("/api/tenants/" + tenantId + "/environments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(createRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
String environmentId = objectMapper.readTree(createResult.getResponse().getContentAsString())
|
||||
.get("id").asText();
|
||||
|
||||
var updateRequest = new UpdateEnvironmentRequest("QA Updated");
|
||||
|
||||
mockMvc.perform(patch("/api/tenants/" + tenantId + "/environments/" + environmentId)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(updateRequest)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.displayName").value("QA Updated"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteDefaultEnvironment_shouldReturn403() throws Exception {
|
||||
var request = new CreateEnvironmentRequest("default", "Default");
|
||||
|
||||
var createResult = mockMvc.perform(post("/api/tenants/" + tenantId + "/environments")
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin")))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
String environmentId = objectMapper.readTree(createResult.getResponse().getContentAsString())
|
||||
.get("id").asText();
|
||||
|
||||
mockMvc.perform(delete("/api/tenants/" + tenantId + "/environments/" + environmentId)
|
||||
.with(jwt().jwt(j -> j.claim("sub", "test-user"))
|
||||
.authorities(new SimpleGrantedAuthority("SCOPE_apps:manage"),
|
||||
new SimpleGrantedAuthority("SCOPE_platform:admin"))))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createEnvironment_noAuth_shouldReturn401() throws Exception {
|
||||
var request = new CreateEnvironmentRequest("no-auth", "No Auth");
|
||||
|
||||
mockMvc.perform(post("/api/tenants/" + tenantId + "/environments")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.environment;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.license.LicenseEntity;
|
||||
import net.siegeln.cameleer.saas.license.LicenseRepository;
|
||||
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 java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EnvironmentServiceTest {
|
||||
|
||||
@Mock
|
||||
private EnvironmentRepository environmentRepository;
|
||||
|
||||
@Mock
|
||||
private LicenseRepository licenseRepository;
|
||||
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
private EnvironmentService environmentService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
environmentService = new EnvironmentService(environmentRepository, licenseRepository, auditService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_shouldCreateEnvironmentAndLogAudit() {
|
||||
var tenantId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
var license = new LicenseEntity();
|
||||
license.setTenantId(tenantId);
|
||||
license.setTier("HIGH");
|
||||
|
||||
when(environmentRepository.existsByTenantIdAndSlug(tenantId, "prod")).thenReturn(false);
|
||||
when(licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId))
|
||||
.thenReturn(Optional.of(license));
|
||||
when(environmentRepository.countByTenantId(tenantId)).thenReturn(0L);
|
||||
when(environmentRepository.save(any(EnvironmentEntity.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = environmentService.create(tenantId, "prod", "Production", actorId);
|
||||
|
||||
assertThat(result.getSlug()).isEqualTo("prod");
|
||||
assertThat(result.getDisplayName()).isEqualTo("Production");
|
||||
assertThat(result.getTenantId()).isEqualTo(tenantId);
|
||||
|
||||
var actionCaptor = ArgumentCaptor.forClass(AuditAction.class);
|
||||
verify(auditService).log(any(), any(), any(), actionCaptor.capture(), any(), any(), any(), any(), any());
|
||||
assertThat(actionCaptor.getValue()).isEqualTo(AuditAction.ENVIRONMENT_CREATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_shouldRejectDuplicateSlug() {
|
||||
var tenantId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
|
||||
when(environmentRepository.existsByTenantIdAndSlug(tenantId, "prod")).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> environmentService.create(tenantId, "prod", "Production", actorId))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_shouldEnforceTierLimit() {
|
||||
var tenantId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
var license = new LicenseEntity();
|
||||
license.setTenantId(tenantId);
|
||||
license.setTier("LOW");
|
||||
|
||||
when(environmentRepository.existsByTenantIdAndSlug(tenantId, "staging")).thenReturn(false);
|
||||
when(licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId))
|
||||
.thenReturn(Optional.of(license));
|
||||
when(environmentRepository.countByTenantId(tenantId)).thenReturn(1L);
|
||||
|
||||
assertThatThrownBy(() -> environmentService.create(tenantId, "staging", "Staging", actorId))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listByTenantId_shouldReturnEnvironments() {
|
||||
var tenantId = UUID.randomUUID();
|
||||
var env1 = new EnvironmentEntity();
|
||||
env1.setSlug("default");
|
||||
var env2 = new EnvironmentEntity();
|
||||
env2.setSlug("prod");
|
||||
|
||||
when(environmentRepository.findByTenantId(tenantId)).thenReturn(List.of(env1, env2));
|
||||
|
||||
var result = environmentService.listByTenantId(tenantId);
|
||||
|
||||
assertThat(result).hasSize(2);
|
||||
assertThat(result).extracting(EnvironmentEntity::getSlug).containsExactly("default", "prod");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getById_shouldReturnEnvironment() {
|
||||
var id = UUID.randomUUID();
|
||||
var env = new EnvironmentEntity();
|
||||
env.setSlug("prod");
|
||||
|
||||
when(environmentRepository.findById(id)).thenReturn(Optional.of(env));
|
||||
|
||||
var result = environmentService.getById(id);
|
||||
|
||||
assertThat(result).isPresent();
|
||||
assertThat(result.get().getSlug()).isEqualTo("prod");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateDisplayName_shouldUpdateAndLogAudit() {
|
||||
var environmentId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
var env = new EnvironmentEntity();
|
||||
env.setSlug("prod");
|
||||
env.setDisplayName("Old Name");
|
||||
env.setTenantId(UUID.randomUUID());
|
||||
|
||||
when(environmentRepository.findById(environmentId)).thenReturn(Optional.of(env));
|
||||
when(environmentRepository.save(any(EnvironmentEntity.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = environmentService.updateDisplayName(environmentId, "New Name", actorId);
|
||||
|
||||
assertThat(result.getDisplayName()).isEqualTo("New Name");
|
||||
|
||||
var actionCaptor = ArgumentCaptor.forClass(AuditAction.class);
|
||||
verify(auditService).log(any(), any(), any(), actionCaptor.capture(), any(), any(), any(), any(), any());
|
||||
assertThat(actionCaptor.getValue()).isEqualTo(AuditAction.ENVIRONMENT_UPDATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete_shouldRejectDefaultEnvironment() {
|
||||
var environmentId = UUID.randomUUID();
|
||||
var actorId = UUID.randomUUID();
|
||||
var env = new EnvironmentEntity();
|
||||
env.setSlug("default");
|
||||
|
||||
when(environmentRepository.findById(environmentId)).thenReturn(Optional.of(env));
|
||||
|
||||
assertThatThrownBy(() -> environmentService.delete(environmentId, actorId))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDefaultForTenant_shouldCreateWithDefaultSlug() {
|
||||
var tenantId = UUID.randomUUID();
|
||||
|
||||
when(environmentRepository.findByTenantIdAndSlug(tenantId, "default")).thenReturn(Optional.empty());
|
||||
when(environmentRepository.existsByTenantIdAndSlug(tenantId, "default")).thenReturn(false);
|
||||
when(environmentRepository.save(any(EnvironmentEntity.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = environmentService.createDefaultForTenant(tenantId);
|
||||
|
||||
assertThat(result.getSlug()).isEqualTo("default");
|
||||
assertThat(result.getDisplayName()).isEqualTo("Default");
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.runtime;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class DockerRuntimeOrchestratorTest {
|
||||
|
||||
@Test
|
||||
void runtimeConfig_parseMemoryLimitBytes_megabytes() {
|
||||
assertEquals(512 * 1024 * 1024L, parseMemoryLimit("512m"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runtimeConfig_parseMemoryLimitBytes_gigabytes() {
|
||||
assertEquals(1024L * 1024 * 1024, parseMemoryLimit("1g"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runtimeConfig_parseMemoryLimitBytes_bytes() {
|
||||
assertEquals(536870912L, parseMemoryLimit("536870912"));
|
||||
}
|
||||
|
||||
private long parseMemoryLimit(String limit) {
|
||||
var l = limit.trim().toLowerCase();
|
||||
if (l.endsWith("g")) {
|
||||
return Long.parseLong(l.substring(0, l.length() - 1)) * 1024 * 1024 * 1024;
|
||||
} else if (l.endsWith("m")) {
|
||||
return Long.parseLong(l.substring(0, l.length() - 1)) * 1024 * 1024;
|
||||
}
|
||||
return Long.parseLong(l);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package net.siegeln.cameleer.saas.tenant;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.environment.EnvironmentService;
|
||||
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
||||
import net.siegeln.cameleer.saas.tenant.dto.CreateTenantRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -33,14 +32,11 @@ class TenantServiceTest {
|
||||
@Mock
|
||||
private LogtoManagementClient logtoClient;
|
||||
|
||||
@Mock
|
||||
private EnvironmentService environmentService;
|
||||
|
||||
private TenantService tenantService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
tenantService = new TenantService(tenantRepository, auditService, logtoClient, environmentService);
|
||||
tenantService = new TenantService(tenantRepository, auditService, logtoClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user