feat: add app controller with multipart JAR upload
Adds AppController at /api/environments/{environmentId}/apps with POST (multipart
metadata+JAR), GET list, GET by ID, PUT jar reupload, and DELETE endpoints.
Also adds CreateAppRequest and AppResponse DTOs, integration tests (AppControllerTest),
and fixes ClickHouseConfig to be excluded in test profile via @Profile("!test").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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 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");
|
||||
env.setBootstrapToken("test-bootstrap-token");
|
||||
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"))))
|
||||
.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"))))
|
||||
.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"))))
|
||||
.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"))))
|
||||
.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"))))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user