feat: add svn preset management and optimize docker builds
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package com.svnlog.web.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import com.svnlog.web.service.AiWorkflowService;
|
||||
import com.svnlog.web.service.HealthService;
|
||||
import com.svnlog.web.service.OutputFileService;
|
||||
import com.svnlog.web.service.RepositoryConfigService;
|
||||
import com.svnlog.web.service.SettingsService;
|
||||
import com.svnlog.web.service.SvnPresetService;
|
||||
import com.svnlog.web.service.SvnWorkflowService;
|
||||
import com.svnlog.web.service.TaskService;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class AppControllerPresetTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldRejectUpdateWithBlankName() throws Exception {
|
||||
buildMockMvc()
|
||||
.perform(put("/api/svn/presets/some-id")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"name\":\"\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error").value("名称不能为空"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUpdateWithBlankUrl() throws Exception {
|
||||
buildMockMvc()
|
||||
.perform(put("/api/svn/presets/some-id")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"url\":\"\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error").value("SVN URL 不能为空"));
|
||||
}
|
||||
|
||||
private MockMvc buildMockMvc() {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
final RepositoryConfigService repoConfigService = new RepositoryConfigService(outputFileService);
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repoConfigService);
|
||||
final AppController controller = new AppController(
|
||||
Mockito.mock(SvnWorkflowService.class),
|
||||
Mockito.mock(AiWorkflowService.class),
|
||||
Mockito.mock(TaskService.class),
|
||||
outputFileService,
|
||||
Mockito.mock(SettingsService.class),
|
||||
svnPresetService,
|
||||
Mockito.mock(HealthService.class)
|
||||
);
|
||||
return MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,6 @@ class AiWorkflowServiceTest {
|
||||
private SettingsService buildSettingsService(OutputFileService outputFileService) {
|
||||
final RepositoryConfigService repositoryConfigService = buildRepositoryConfigService(outputFileService);
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
return new SettingsService(outputFileService, new SettingsPersistenceService(), svnPresetService, repositoryConfigService);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ class HealthServiceTest {
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
final SettingsService settingsService = new SettingsService(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
|
||||
@@ -42,6 +42,11 @@ class SettingsServiceTest {
|
||||
void shouldPersistAndReloadSettingsFromOutputDirectory() throws IOException {
|
||||
useTempWorkingDirectory();
|
||||
final Path customOutputDir = tempDir.resolve("custom-output");
|
||||
final Path initialOutputDir = tempDir.resolve("outputs");
|
||||
|
||||
// 创建预设 id=preset-2,用于 defaultSvnPresetId 校验
|
||||
writeRepositoryConfigToDir(initialOutputDir, "preset-2", "Preset 2", "https://svn.example.com/p2");
|
||||
writeRepositoryConfigToDir(customOutputDir, "preset-2", "Preset 2", "https://svn.example.com/p2");
|
||||
|
||||
final SettingsService settingsService = newSettingsService();
|
||||
settingsService.updateSettings(
|
||||
@@ -199,6 +204,45 @@ class SettingsServiceTest {
|
||||
assertNotNull(settings.get("apiKeySource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreferPresetCredentialsOverGlobalWhenBothExist() throws IOException {
|
||||
useTempWorkingDirectory();
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
writeRepositoryConfig(outputFileService.getOutputRoot().resolve("repository-configs.json"));
|
||||
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
|
||||
final SettingsService settingsService = new SettingsService(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
svnPresetService,
|
||||
repositoryConfigService
|
||||
);
|
||||
// 设置全局 SVN 凭据(与预设不同)
|
||||
settingsService.updateSettings(
|
||||
"deepseek-key", // apiKey
|
||||
"deepseek", // provider
|
||||
null, // openaiBaseUrl
|
||||
null, // openaiApiKey
|
||||
null, // openaiStageOneModel
|
||||
null, // openaiStageTwoModel
|
||||
"global-user", // svnUsername(全局)
|
||||
"global-pass", // svnPassword(全局)
|
||||
null, // outputDir
|
||||
null // defaultSvnPresetId
|
||||
);
|
||||
|
||||
// 预设 json-user/json-pass 应优先于全局 global-user/global-pass
|
||||
final SettingsService.SvnCredentials credentials =
|
||||
settingsService.resolveSvnCredentials(null, null, "preset-json");
|
||||
|
||||
assertEquals("json-user", credentials.getUsername());
|
||||
assertEquals("json-pass", credentials.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveSvnCredentialsFromRepositoryConfigWhenSettingsAreEmpty() throws IOException {
|
||||
useTempWorkingDirectory();
|
||||
@@ -209,7 +253,6 @@ class SettingsServiceTest {
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
final SettingsService settingsService = new SettingsService(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
@@ -233,10 +276,24 @@ class SettingsServiceTest {
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
return new SettingsService(outputFileService, settingsPersistenceService, svnPresetService, repositoryConfigService);
|
||||
}
|
||||
|
||||
private void writeRepositoryConfigToDir(Path outputDir, String id, String name, String url) throws IOException {
|
||||
final RepositoryConfig config = new RepositoryConfig();
|
||||
config.setId(id);
|
||||
config.setName(name);
|
||||
config.setType("SVN");
|
||||
config.setEnabled(true);
|
||||
config.setSvnUrl(url);
|
||||
config.setSvnUsername("");
|
||||
final Path configPath = outputDir.resolve("repository-configs.json");
|
||||
Files.createDirectories(configPath.getParent());
|
||||
final String json = new GsonBuilder().setPrettyPrinting().create()
|
||||
.toJson(java.util.Collections.singletonList(config));
|
||||
Files.write(configPath, json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void writeRepositoryConfig(Path configPath) throws IOException {
|
||||
final RepositoryConfig config = new RepositoryConfig();
|
||||
config.setId("preset-json");
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.svnlog.web.dto.SvnPresetCreateRequest;
|
||||
import com.svnlog.web.dto.SvnPresetUpdateRequest;
|
||||
import com.svnlog.web.model.RepositoryConfig;
|
||||
import com.svnlog.web.model.SvnPresetManageItem;
|
||||
import com.svnlog.web.util.CryptoUtils;
|
||||
|
||||
public class SvnPresetServiceTest {
|
||||
|
||||
private static RepositoryConfigService createRepoService(Path outputRoot) throws IOException {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(outputRoot.toString());
|
||||
return new RepositoryConfigService(outputFileService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreatePresetAndPersistToJson() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
final SvnPresetCreateRequest request = new SvnPresetCreateRequest();
|
||||
request.setName("test-project");
|
||||
request.setUrl("https://svn.example.com/test");
|
||||
request.setSvnUsername("testuser");
|
||||
request.setSvnPassword("secret123");
|
||||
|
||||
final SvnPresetManageItem result = service.create(request);
|
||||
Assertions.assertNotNull(result.getId());
|
||||
Assertions.assertEquals("test-project", result.getName());
|
||||
Assertions.assertEquals("https://svn.example.com/test", result.getUrl());
|
||||
Assertions.assertEquals("testuser", result.getSvnUsername());
|
||||
Assertions.assertTrue(result.isSvnCredentialsConfigured());
|
||||
Assertions.assertTrue(result.isEnabled());
|
||||
|
||||
// 验证密码加密保存且不回显明文
|
||||
final RepositoryConfig raw = repoService.getById(result.getId());
|
||||
Assertions.assertFalse(raw.getSvnPasswordEncrypted().isEmpty());
|
||||
Assertions.assertNotEquals("secret123", raw.getSvnPasswordEncrypted());
|
||||
Assertions.assertEquals("secret123", CryptoUtils.decrypt(raw.getSvnPasswordEncrypted()));
|
||||
|
||||
// 验证管理列表返回用户名但不返回密码
|
||||
final SvnPresetManageItem manageItem = service.listManage().get(0);
|
||||
Assertions.assertEquals("testuser", manageItem.getSvnUsername());
|
||||
// Password is never exposed in the manage item
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUpdatePresetWithoutOverwritingPasswordWhenEmpty() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-update-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
// Create preset with password
|
||||
final SvnPresetCreateRequest request = new SvnPresetCreateRequest();
|
||||
request.setName("project");
|
||||
request.setUrl("https://svn.example.com/project");
|
||||
request.setSvnUsername("user1");
|
||||
request.setSvnPassword("original-password");
|
||||
final SvnPresetManageItem created = service.create(request);
|
||||
|
||||
final String encryptedBefore = repoService.getById(created.getId()).getSvnPasswordEncrypted();
|
||||
|
||||
// Update without password — should not overwrite
|
||||
final SvnPresetUpdateRequest updateReq = new SvnPresetUpdateRequest();
|
||||
updateReq.setName("updated-project");
|
||||
updateReq.setUrl("https://svn.example.com/updated");
|
||||
updateReq.setSvnUsername("user2");
|
||||
// svnPassword is null — should not overwrite
|
||||
updateReq.setEnabled(true);
|
||||
|
||||
service.update(created.getId(), updateReq);
|
||||
|
||||
final RepositoryConfig updated = repoService.getById(created.getId());
|
||||
Assertions.assertEquals("updated-project", updated.getName());
|
||||
Assertions.assertEquals("https://svn.example.com/updated", updated.getSvnUrl());
|
||||
Assertions.assertEquals("user2", updated.getSvnUsername());
|
||||
// Password should remain unchanged
|
||||
Assertions.assertEquals(encryptedBefore, updated.getSvnPasswordEncrypted());
|
||||
Assertions.assertEquals("original-password", CryptoUtils.decrypt(updated.getSvnPasswordEncrypted()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotOverwriteUsernameWithEmptyString() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-username-empty-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
final SvnPresetCreateRequest request = new SvnPresetCreateRequest();
|
||||
request.setName("project");
|
||||
request.setUrl("https://svn.example.com/project");
|
||||
request.setSvnUsername("existing-user");
|
||||
request.setSvnPassword("pass");
|
||||
final SvnPresetManageItem created = service.create(request);
|
||||
|
||||
// Update with empty username — should NOT overwrite
|
||||
final SvnPresetUpdateRequest updateReq = new SvnPresetUpdateRequest();
|
||||
updateReq.setSvnUsername("");
|
||||
service.update(created.getId(), updateReq);
|
||||
|
||||
final RepositoryConfig updated = repoService.getById(created.getId());
|
||||
Assertions.assertEquals("existing-user", updated.getSvnUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotOverwriteUsernameWithWhitespace() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-username-whitespace-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
final SvnPresetCreateRequest request = new SvnPresetCreateRequest();
|
||||
request.setName("project");
|
||||
request.setUrl("https://svn.example.com/project");
|
||||
request.setSvnUsername("existing-user");
|
||||
request.setSvnPassword("pass");
|
||||
final SvnPresetManageItem created = service.create(request);
|
||||
|
||||
// Update with whitespace-only username — should NOT overwrite
|
||||
final SvnPresetUpdateRequest updateReq = new SvnPresetUpdateRequest();
|
||||
updateReq.setSvnUsername(" ");
|
||||
service.update(created.getId(), updateReq);
|
||||
|
||||
final RepositoryConfig updated = repoService.getById(created.getId());
|
||||
Assertions.assertEquals("existing-user", updated.getSvnUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUpdateUsernameWhenValid() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-username-valid-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
final SvnPresetCreateRequest request = new SvnPresetCreateRequest();
|
||||
request.setName("project");
|
||||
request.setUrl("https://svn.example.com/project");
|
||||
request.setSvnUsername("old-user");
|
||||
request.setSvnPassword("pass");
|
||||
final SvnPresetManageItem created = service.create(request);
|
||||
|
||||
// Update with valid non-empty username
|
||||
final SvnPresetUpdateRequest updateReq = new SvnPresetUpdateRequest();
|
||||
updateReq.setSvnUsername("new-user");
|
||||
service.update(created.getId(), updateReq);
|
||||
|
||||
final RepositoryConfig updated = repoService.getById(created.getId());
|
||||
Assertions.assertEquals("new-user", updated.getSvnUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldClearPasswordWhenClearSvnPasswordIsTrue() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-clear-pwd-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
final SvnPresetCreateRequest request = new SvnPresetCreateRequest();
|
||||
request.setName("project");
|
||||
request.setUrl("https://svn.example.com/project");
|
||||
request.setSvnPassword("sekret");
|
||||
final SvnPresetManageItem created = service.create(request);
|
||||
|
||||
// Clear password
|
||||
final SvnPresetUpdateRequest updateReq = new SvnPresetUpdateRequest();
|
||||
updateReq.setClearSvnPassword(true);
|
||||
service.update(created.getId(), updateReq);
|
||||
|
||||
final RepositoryConfig updated = repoService.getById(created.getId());
|
||||
Assertions.assertTrue(updated.getSvnPasswordEncrypted() == null
|
||||
|| updated.getSvnPasswordEncrypted().isEmpty());
|
||||
Assertions.assertFalse(service.listManage().get(0).isSvnCredentialsConfigured());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSoftDeletePreset() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-delete-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
final SvnPresetCreateRequest request = new SvnPresetCreateRequest();
|
||||
request.setName("to-delete");
|
||||
request.setUrl("https://svn.example.com/todelete");
|
||||
final SvnPresetManageItem created = service.create(request);
|
||||
|
||||
Assertions.assertTrue(created.isEnabled());
|
||||
|
||||
service.delete(created.getId());
|
||||
|
||||
// After soft delete, disabled item should not appear in summary list
|
||||
final java.util.List<com.svnlog.web.model.SvnPresetSummary> summaries = service.listPresetSummaries();
|
||||
Assertions.assertTrue(summaries.isEmpty());
|
||||
|
||||
// But still visible in manage list with enabled=false
|
||||
final SvnPresetManageItem manageItem = service.listManage().get(0);
|
||||
Assertions.assertFalse(manageItem.isEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnEmptyWhenNoPresetsConfigured() throws Exception {
|
||||
final Path tempDir = Files.createTempDirectory("svn-preset-empty-test");
|
||||
final RepositoryConfigService repoService = createRepoService(tempDir);
|
||||
final SvnPresetService service = new SvnPresetService(repoService);
|
||||
|
||||
Assertions.assertTrue(service.listPresetSummaries().isEmpty());
|
||||
Assertions.assertTrue(service.listManage().isEmpty());
|
||||
Assertions.assertEquals("", service.firstPresetId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user