+
diff --git a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/ActiveThingifierWorkspaceTest.java b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/ActiveThingifierWorkspaceTest.java
index b7be17c0..96d4c69b 100644
--- a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/ActiveThingifierWorkspaceTest.java
+++ b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/ActiveThingifierWorkspaceTest.java
@@ -1,10 +1,17 @@
package uk.co.compendiumdev.thingifier.crudui;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.nio.file.Files;
+import java.nio.file.Path;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
public class ActiveThingifierWorkspaceTest {
+ @TempDir Path temp;
+
@Test
public void defaultWorkspaceStartsWithTodoManager() {
try (ActiveThingifierWorkspace workspace =
@@ -31,4 +38,80 @@ public void yamlLoadReplacesSchemaAndIncrementsVersion() {
Assertions.assertNull(snapshot.definition().entityNamed("project"));
}
}
+
+ @Test
+ public void switchToSqliteMemoryMigratesDataAndRelationships() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ workspace.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(workspace));
+
+ workspace.switchStorage(WorkspaceStorage.sqliteMemory());
+
+ DynamicThingifierApiProxy proxy = new DynamicThingifierApiProxy(workspace);
+ Assertions.assertEquals("sqlite-memory", workspace.snapshot().storage().mode());
+ Assertions.assertTrue(proxy.getJson("projects").body().contains("Project A"));
+ Assertions.assertTrue(proxy.getJson("todos").body().contains("Task A"));
+ Assertions.assertEquals(
+ 1,
+ root(proxy.getJson("projects/1/tasks").body()).getAsJsonArray("todos").size());
+ }
+ }
+
+ @Test
+ public void switchToSqliteFileCreatesFileAndMigratesData() {
+ Path databaseFile = temp.resolve("workspace.sqlite");
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ workspace.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(workspace));
+
+ workspace.switchStorage(WorkspaceStorage.sqliteFile(databaseFile));
+
+ DynamicThingifierApiProxy proxy = new DynamicThingifierApiProxy(workspace);
+ Assertions.assertTrue(Files.exists(databaseFile));
+ Assertions.assertEquals("sqlite-file", workspace.snapshot().storage().mode());
+ Assertions.assertTrue(proxy.getJson("projects").body().contains("Project A"));
+ Assertions.assertEquals(
+ 1,
+ root(proxy.getJson("projects/1/tasks").body()).getAsJsonArray("todos").size());
+ }
+ }
+
+ @Test
+ public void switchFromSqliteFileBackToMemoryMigratesData() {
+ Path databaseFile = temp.resolve("workspace.sqlite");
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ workspace.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(workspace));
+ workspace.switchStorage(WorkspaceStorage.sqliteFile(databaseFile));
+
+ workspace.switchStorage(WorkspaceStorage.memory());
+
+ DynamicThingifierApiProxy proxy = new DynamicThingifierApiProxy(workspace);
+ Assertions.assertEquals("memory", workspace.snapshot().storage().mode());
+ Assertions.assertTrue(proxy.getJson("projects").body().contains("Project A"));
+ Assertions.assertEquals(
+ 1,
+ root(proxy.getJson("projects/1/tasks").body()).getAsJsonArray("todos").size());
+ }
+ }
+
+ private void createRelatedProjectAndTodo(final DynamicThingifierApiProxy proxy) {
+ String projectId =
+ field(proxy.postJson("projects", "{\"title\":\"Project A\"}").body(), "id");
+ String todoId = field(proxy.postJson("todos", "{\"title\":\"Task A\"}").body(), "id");
+ UiHttpResponse relationship =
+ proxy.postJson("projects/" + projectId + "/tasks", "{\"id\":\"" + todoId + "\"}");
+ Assertions.assertEquals(201, relationship.statusCode());
+ }
+
+ private String field(final String json, final String fieldName) {
+ return root(json).get(fieldName).getAsString();
+ }
+
+ private JsonObject root(final String json) {
+ return JsonParser.parseString(json).getAsJsonObject();
+ }
}
diff --git a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiArgumentsTest.java b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiArgumentsTest.java
index fb5dff13..f5908373 100644
--- a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiArgumentsTest.java
+++ b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiArgumentsTest.java
@@ -23,4 +23,18 @@ public void rejectsProjectAndModelYamlTogether() {
CrudUiArguments.parse(
new String[] {"-project=project-folder", "-modelYaml=model.yaml"}));
}
+
+ @Test
+ public void parsesStorageModeAndSqliteFilePath() {
+ CrudUiArguments arguments =
+ CrudUiArguments.parse(
+ new String[] {
+ "-modelYaml=model.yaml",
+ "-thingifier-repository=sqlite-file",
+ "-thingifier-sqlite-file=data.sqlite"
+ });
+
+ Assertions.assertEquals("sqlite-file", arguments.storage().mode());
+ Assertions.assertTrue(arguments.storage().sqliteFilePath().endsWith("data.sqlite"));
+ }
}
diff --git a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiControllerTest.java b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiControllerTest.java
index 4e172f4e..83a7ad8b 100644
--- a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiControllerTest.java
+++ b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/CrudUiControllerTest.java
@@ -2,11 +2,16 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
+import java.nio.file.Path;
+import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
public class CrudUiControllerTest {
+ @TempDir Path temp;
+
@Test
public void workspaceRouteReturnsSchemaMetadata() {
try (ActiveThingifierWorkspace workspace =
@@ -15,11 +20,107 @@ public void workspaceRouteReturnsSchemaMetadata() {
UiHttpResponse response = controller.workspace();
- Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertEquals(200, response.statusCode(), response.body());
Assertions.assertTrue(response.body().contains("\"entities\""));
Assertions.assertTrue(response.body().contains("\"relationships\""));
Assertions.assertTrue(response.body().contains("\"schemaYaml\""));
Assertions.assertTrue(response.body().contains("\"project\""));
+ Assertions.assertTrue(response.body().contains("\"storage\""));
+ }
+ }
+
+ @Test
+ public void storageSwitchEndpointChangesWorkspaceStorageMode() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ CrudUiController controller = new CrudUiController(workspace);
+ Path databaseFile = temp.resolve("controller.sqlite");
+
+ UiHttpResponse response =
+ controller.switchStorage(
+ JsonSupport.toJson(
+ Map.of(
+ "mode",
+ "sqlite-file",
+ "sqliteFile",
+ databaseFile.toString())));
+
+ Assertions.assertEquals(200, response.statusCode(), response.body());
+ Assertions.assertEquals("sqlite-file", workspace.snapshot().storage().mode());
+ Assertions.assertTrue(response.body().contains("\"storageStatus\": \"switched\""));
+ }
+ }
+
+ @Test
+ public void projectBrowseEndpointReturnsSelectedPathFromChooser() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ Path chosen = temp.resolve("chosen-project");
+ CrudUiController controller =
+ new CrudUiController(
+ workspace, request -> ProjectPathSelection.selected(chosen.toString()));
+
+ UiHttpResponse response =
+ controller.browseProject(
+ JsonSupport.toJson(Map.of("action", "save", "path", "")));
+ JsonObject body = JsonParser.parseString(response.body()).getAsJsonObject();
+
+ Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertTrue(body.get("selected").getAsBoolean());
+ Assertions.assertEquals(chosen.toString(), body.get("path").getAsString());
+ }
+ }
+
+ @Test
+ public void projectBrowseEndpointReturnsCancelledSelection() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ CrudUiController controller =
+ new CrudUiController(workspace, request -> ProjectPathSelection.cancelled());
+
+ UiHttpResponse response =
+ controller.browseProject(
+ JsonSupport.toJson(Map.of("action", "load", "path", "")));
+ JsonObject body = JsonParser.parseString(response.body()).getAsJsonObject();
+
+ Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertFalse(body.get("selected").getAsBoolean());
+ Assertions.assertEquals(
+ "Project browsing cancelled.", body.get("message").getAsString());
+ }
+ }
+
+ @Test
+ public void projectBrowseEndpointReportsUnavailableChooserAsBadRequest() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ CrudUiController controller =
+ new CrudUiController(
+ workspace,
+ request -> ProjectPathSelection.unavailable("Browse unavailable"));
+
+ UiHttpResponse response =
+ controller.browseProject(
+ JsonSupport.toJson(Map.of("action", "save", "path", "")));
+
+ Assertions.assertEquals(400, response.statusCode());
+ Assertions.assertTrue(response.body().contains("Browse unavailable"));
+ }
+ }
+
+ @Test
+ public void sqliteFileStorageSwitchRequiresAFilePathAndDoesNotMutateWorkspace() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ CrudUiController controller = new CrudUiController(workspace);
+ long version = workspace.snapshot().version();
+
+ UiHttpResponse response =
+ controller.switchStorage(JsonSupport.toJson(Map.of("mode", "sqlite-file")));
+
+ Assertions.assertEquals(400, response.statusCode());
+ Assertions.assertEquals(version, workspace.snapshot().version());
+ Assertions.assertEquals("memory", workspace.snapshot().storage().mode());
}
}
@@ -109,6 +210,14 @@ public void staticIndexResourceIsAvailable() {
Assertions.assertTrue(index.contains("Load Project"));
Assertions.assertTrue(index.contains("id=\"project-dialog\""));
Assertions.assertTrue(index.contains("id=\"project-path-input\""));
+ Assertions.assertTrue(index.contains("id=\"project-recent-paths\""));
+ Assertions.assertTrue(index.contains("id=\"project-browse-button\""));
+ Assertions.assertTrue(index.contains("id=\"project-validate-button\""));
+ Assertions.assertTrue(index.contains("id=\"project-browser-save-button\""));
+ Assertions.assertTrue(index.contains("id=\"project-browser-load-button\""));
+ Assertions.assertTrue(index.contains("Browser Save..."));
+ Assertions.assertTrue(index.contains("Browser Load..."));
+ Assertions.assertTrue(index.contains("Validate Path"));
Assertions.assertTrue(index.contains("Workspace"));
Assertions.assertTrue(index.contains("Schema Edit"));
Assertions.assertTrue(index.contains("href=\"/schema\""));
@@ -280,6 +389,20 @@ public void staticAssetsContainExpandableRelationshipOutline() {
Assertions.assertTrue(script.contains("/ui/schema/upgrade/apply"));
Assertions.assertTrue(script.contains("/ui/project/save"));
Assertions.assertTrue(script.contains("/ui/project/load"));
+ Assertions.assertTrue(script.contains("/ui/project/browse"));
+ Assertions.assertTrue(script.contains("/ui/project/check"));
+ Assertions.assertTrue(script.contains("/ui/project/load-files"));
+ Assertions.assertTrue(script.contains("/ui/project/export-files"));
+ Assertions.assertTrue(script.contains("browserSaveProject"));
+ Assertions.assertTrue(script.contains("browserLoadProject"));
+ Assertions.assertTrue(script.contains("validateProjectPath"));
+ Assertions.assertTrue(script.contains("projectRecentPaths"));
+ Assertions.assertTrue(script.contains("showDirectoryPicker"));
+ Assertions.assertTrue(script.contains("Browser folder picker is not available"));
+ Assertions.assertTrue(script.contains("/ui/storage/switch"));
+ Assertions.assertTrue(script.contains("storageModeSelect"));
+ Assertions.assertTrue(script.contains("storageFileInput"));
+ Assertions.assertTrue(script.contains("Storage switched."));
Assertions.assertTrue(script.contains("openProjectDialog"));
Assertions.assertTrue(script.contains("Project saved."));
Assertions.assertTrue(script.contains("Project loaded."));
@@ -333,6 +456,9 @@ public void staticAssetsContainExpandableRelationshipOutline() {
Assertions.assertTrue(styles.contains(".schema-dialog-footer"));
Assertions.assertTrue(styles.contains(".project-dialog"));
Assertions.assertTrue(styles.contains(".project-dialog-warning"));
+ Assertions.assertTrue(styles.contains(".project-path-controls"));
+ Assertions.assertTrue(styles.contains(".project-browser-actions"));
+ Assertions.assertTrue(styles.contains(".project-browser-status"));
Assertions.assertTrue(styles.contains(".schema-upgrade-layout"));
Assertions.assertTrue(styles.contains(".schema-upgrade-row"));
Assertions.assertTrue(styles.contains(".schema-upgrade-summary"));
diff --git a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/WorkspaceProjectServiceTest.java b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/WorkspaceProjectServiceTest.java
index 119718f9..b37a72cb 100644
--- a/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/WorkspaceProjectServiceTest.java
+++ b/thingifier-crud-ui/src/test/java/uk/co/compendiumdev/thingifier/crudui/WorkspaceProjectServiceTest.java
@@ -4,6 +4,10 @@
import com.google.gson.JsonParser;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -65,6 +69,176 @@ public void savedProjectReloadsSchemaDataAndRelationshipEdges() {
}
}
+ @Test
+ public void sqliteBackedSaveCreatesManifestSchemaAndDatabaseFile() throws Exception {
+ Path projectFolder = temp.resolve("sqlite-project-bundle");
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ workspace.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(workspace));
+ workspace.switchStorage(WorkspaceStorage.sqliteFile(temp.resolve("source.sqlite")));
+
+ UiHttpResponse response =
+ new CrudUiController(workspace).saveProject(request(projectFolder));
+
+ Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertTrue(Files.exists(projectFolder.resolve("projectfile.erproj")));
+ Assertions.assertTrue(Files.exists(projectFolder.resolve("schema.yaml")));
+ Assertions.assertTrue(Files.exists(projectFolder.resolve("data.sqlite")));
+ Assertions.assertFalse(Files.exists(projectFolder.resolve("data.json")));
+ Assertions.assertTrue(
+ Files.readString(projectFolder.resolve("projectfile.erproj"))
+ .contains("dataFile: data.sqlite"));
+ Assertions.assertFalse(
+ Files.readString(projectFolder.resolve("projectfile.erproj"))
+ .contains("storage:"));
+ }
+ }
+
+ @Test
+ public void saveAsSqliteFromMemorySwitchesWorkspaceToProjectDatabaseFile() throws Exception {
+ Path projectFolder = temp.resolve("sqlite-save-as-project");
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ workspace.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(workspace));
+
+ UiHttpResponse response =
+ new CrudUiController(workspace).saveProject(request(projectFolder, "sqlite"));
+
+ Assertions.assertEquals(200, response.statusCode(), response.body());
+ Assertions.assertTrue(Files.exists(projectFolder.resolve("data.sqlite")));
+ Assertions.assertFalse(Files.exists(projectFolder.resolve("data.json")));
+ Assertions.assertEquals("sqlite-file", workspace.snapshot().storage().mode());
+ Assertions.assertEquals(
+ projectFolder.resolve("data.sqlite").toAbsolutePath().normalize().toString(),
+ workspace.snapshot().storage().sqliteFilePath());
+ Assertions.assertTrue(
+ new DynamicThingifierApiProxy(workspace)
+ .getJson("projects")
+ .body()
+ .contains("Project A"));
+ }
+ }
+
+ @Test
+ public void saveAsJsonFromSqliteSwitchesWorkspaceToInMemoryStorage() throws Exception {
+ Path projectFolder = temp.resolve("json-save-as-project");
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ workspace.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(workspace));
+ workspace.switchStorage(WorkspaceStorage.sqliteFile(temp.resolve("source.sqlite")));
+
+ UiHttpResponse response =
+ new CrudUiController(workspace).saveProject(request(projectFolder, "json"));
+
+ Assertions.assertEquals(200, response.statusCode(), response.body());
+ Assertions.assertTrue(Files.exists(projectFolder.resolve("data.json")));
+ Assertions.assertFalse(
+ Files.readString(projectFolder.resolve("projectfile.erproj"))
+ .contains("sqlite-file"));
+ Assertions.assertEquals("memory", workspace.snapshot().storage().mode());
+ Assertions.assertTrue(
+ new DynamicThingifierApiProxy(workspace)
+ .getJson("projects")
+ .body()
+ .contains("Project A"));
+ }
+ }
+
+ @Test
+ public void sqliteBackedProjectReloadsDataAndSwitchesStorageMode() {
+ Path projectFolder = temp.resolve("sqlite-project-bundle");
+ try (ActiveThingifierWorkspace source =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ source.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(source));
+ source.switchStorage(WorkspaceStorage.sqliteFile(temp.resolve("source.sqlite")));
+ new CrudUiController(source).saveProject(request(projectFolder));
+ }
+
+ try (ActiveThingifierWorkspace target =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ UiHttpResponse response =
+ new CrudUiController(target).loadProject(request(projectFolder));
+ DynamicThingifierApiProxy targetProxy = new DynamicThingifierApiProxy(target);
+
+ Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertEquals("sqlite-file", target.snapshot().storage().mode());
+ Assertions.assertTrue(
+ target.snapshot().storage().sqliteFilePath().endsWith("data.sqlite"));
+ Assertions.assertTrue(targetProxy.getJson("projects").body().contains("Project A"));
+ Assertions.assertEquals(
+ 1,
+ root(targetProxy.getJson("projects/1/tasks").body())
+ .getAsJsonArray("todos")
+ .size());
+ }
+ }
+
+ @Test
+ public void loadProjectTreatsSqliteDataFileAsSqliteStorageWithoutStorageBlock()
+ throws Exception {
+ Path projectFolder = temp.resolve("edited-sqlite-project-bundle");
+ try (ActiveThingifierWorkspace source =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ source.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(source));
+ source.switchStorage(WorkspaceStorage.sqliteFile(temp.resolve("source.sqlite")));
+ new CrudUiController(source).saveProject(request(projectFolder));
+ }
+ Files.move(
+ projectFolder.resolve("data.sqlite"), projectFolder.resolve("todomanager.sqlite"));
+ Files.writeString(
+ projectFolder.resolve("projectfile.erproj"),
+ "formatVersion: 1\n"
+ + "project:\n"
+ + " title: Edited SQLite Project\n"
+ + "schemaFile: schema.yaml\n"
+ + "dataFile: todomanager.sqlite\n");
+
+ try (ActiveThingifierWorkspace target =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ UiHttpResponse response =
+ new CrudUiController(target).loadProject(request(projectFolder));
+ DynamicThingifierApiProxy targetProxy = new DynamicThingifierApiProxy(target);
+
+ Assertions.assertEquals(200, response.statusCode(), response.body());
+ Assertions.assertEquals("sqlite-file", target.snapshot().storage().mode());
+ Assertions.assertTrue(
+ target.snapshot().storage().sqliteFilePath().endsWith("todomanager.sqlite"));
+ Assertions.assertTrue(targetProxy.getJson("projects").body().contains("Project A"));
+ }
+ }
+
+ @Test
+ public void loadProjectSniffsSqliteDataFileWhenFilenameHasNoSqliteExtension() throws Exception {
+ Path projectFolder = temp.resolve("sniffed-sqlite-project-bundle");
+ try (ActiveThingifierWorkspace source =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ source.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(source));
+ source.switchStorage(WorkspaceStorage.sqliteFile(temp.resolve("source.sqlite")));
+ new CrudUiController(source).saveProject(request(projectFolder));
+ }
+ Files.move(projectFolder.resolve("data.sqlite"), projectFolder.resolve("data.bin"));
+ Files.writeString(
+ projectFolder.resolve("projectfile.erproj"),
+ "formatVersion: 1\n" + "schemaFile: schema.yaml\n" + "dataFile: data.bin\n");
+
+ try (ActiveThingifierWorkspace target =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ UiHttpResponse response =
+ new CrudUiController(target).loadProject(request(projectFolder));
+
+ Assertions.assertEquals(200, response.statusCode(), response.body());
+ Assertions.assertEquals("sqlite-file", target.snapshot().storage().mode());
+ Assertions.assertTrue(
+ target.snapshot().storage().sqliteFilePath().endsWith("data.bin"));
+ }
+ }
+
@Test
public void loadAcceptsDirectProjectFilePath() {
Path projectFolder = temp.resolve("project-bundle");
@@ -85,6 +259,199 @@ public void loadAcceptsDirectProjectFilePath() {
}
}
+ @Test
+ public void checkSavePathReportsCreatableFolderAndManagedFiles() throws Exception {
+ Path creatableFolder = temp.resolve("new-project");
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ CrudUiController controller = new CrudUiController(workspace);
+
+ JsonObject creatable =
+ root(controller.checkProject(actionRequest("save", creatableFolder)).body());
+
+ Assertions.assertTrue(creatable.get("canProceed").getAsBoolean());
+ Assertions.assertEquals("creatable-folder", creatable.get("kind").getAsString());
+
+ Path existingFolder = temp.resolve("existing-project");
+ Files.createDirectories(existingFolder);
+ Files.writeString(existingFolder.resolve("projectfile.erproj"), "managed");
+ Files.writeString(existingFolder.resolve("schema.yaml"), "managed");
+
+ JsonObject existing =
+ root(controller.checkProject(actionRequest("save", existingFolder)).body());
+
+ Assertions.assertTrue(existing.get("canProceed").getAsBoolean());
+ Assertions.assertTrue(
+ existing.get("warning").getAsString().contains("projectfile.erproj"));
+ Assertions.assertTrue(existing.get("warning").getAsString().contains("schema.yaml"));
+ }
+ }
+
+ @Test
+ public void checkLoadPathAcceptsFolderAndDirectProjectFile() {
+ Path projectFolder = temp.resolve("project-bundle");
+ try (ActiveThingifierWorkspace source =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ new CrudUiController(source).saveProject(request(projectFolder));
+ }
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ CrudUiController controller = new CrudUiController(workspace);
+
+ JsonObject folderCheck =
+ root(controller.checkProject(actionRequest("load", projectFolder)).body());
+ JsonObject fileCheck =
+ root(
+ controller
+ .checkProject(
+ actionRequest(
+ "load",
+ projectFolder.resolve("projectfile.erproj")))
+ .body());
+
+ Assertions.assertTrue(folderCheck.get("canProceed").getAsBoolean());
+ Assertions.assertEquals("folder", folderCheck.get("kind").getAsString());
+ Assertions.assertTrue(fileCheck.get("canProceed").getAsBoolean());
+ Assertions.assertEquals("project-file", fileCheck.get("kind").getAsString());
+ }
+ }
+
+ @Test
+ public void browserExportFilesEmitsJsonBackedProjectFiles() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ UiHttpResponse response = new CrudUiController(workspace).exportProjectFiles();
+ JsonObject body = root(response.body());
+
+ Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertTrue(response.body().contains("\"projectStatus\": \"exported\""));
+ Assertions.assertTrue(response.body().contains("\"projectfile.erproj\""));
+ Assertions.assertTrue(response.body().contains("\"schema.yaml\""));
+ Assertions.assertTrue(response.body().contains("\"data.json\""));
+ Assertions.assertFalse(response.body().contains("\"data.sqlite\""));
+ Assertions.assertEquals("memory", body.get("storageMode").getAsString());
+ Assertions.assertEquals("json", body.get("projectStorageMode").getAsString());
+ }
+ }
+
+ @Test
+ public void browserExportFilesEmitsSqliteBackedProjectFiles() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ workspace.switchStorage(WorkspaceStorage.sqliteFile(temp.resolve("source.sqlite")));
+
+ UiHttpResponse response = new CrudUiController(workspace).exportProjectFiles();
+ JsonObject body = root(response.body());
+
+ Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertTrue(response.body().contains("\"projectfile.erproj\""));
+ Assertions.assertTrue(response.body().contains("\"schema.yaml\""));
+ Assertions.assertTrue(response.body().contains("\"data.sqlite\""));
+ Assertions.assertFalse(response.body().contains("\"data.json\""));
+ Assertions.assertEquals("sqlite-file", body.get("storageMode").getAsString());
+ Assertions.assertEquals("sqlite", body.get("projectStorageMode").getAsString());
+ }
+ }
+
+ @Test
+ public void browserExportFilesCanExportSqliteProjectFromMemoryWorkspace() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ UiHttpResponse response =
+ new CrudUiController(workspace)
+ .exportProjectFiles(
+ JsonSupport.toJson(Map.of("projectStorageMode", "sqlite")));
+ JsonObject body = root(response.body());
+
+ Assertions.assertEquals(200, response.statusCode());
+ Assertions.assertTrue(response.body().contains("\"data.sqlite\""));
+ Assertions.assertFalse(response.body().contains("\"data.json\""));
+ Assertions.assertEquals("sqlite-file", body.get("storageMode").getAsString());
+ Assertions.assertEquals("sqlite", body.get("projectStorageMode").getAsString());
+ Assertions.assertEquals("memory", workspace.snapshot().storage().mode());
+ }
+ }
+
+ @Test
+ public void browserLoadFilesLoadsJsonBackedProjectWithoutServerPath() throws Exception {
+ Path projectFolder = temp.resolve("browser-json-project");
+ try (ActiveThingifierWorkspace source =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ source.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(source));
+ new CrudUiController(source).saveProject(request(projectFolder));
+ }
+
+ try (ActiveThingifierWorkspace target =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ UiHttpResponse response =
+ new CrudUiController(target)
+ .loadProjectFiles(browserFilePayload(projectFolder, "browser-json"));
+ DynamicThingifierApiProxy targetProxy = new DynamicThingifierApiProxy(target);
+
+ Assertions.assertEquals(200, response.statusCode(), response.body());
+ Assertions.assertEquals("Project Tasks", target.snapshot().definition().title());
+ Assertions.assertEquals(
+ "Browser folder: browser-json", target.snapshot().projectPath());
+ Assertions.assertTrue(targetProxy.getJson("projects").body().contains("Project A"));
+ Assertions.assertEquals(
+ 1,
+ root(targetProxy.getJson("projects/1/tasks").body())
+ .getAsJsonArray("todos")
+ .size());
+ }
+ }
+
+ @Test
+ public void browserLoadFilesLoadsSqliteBackedProject() throws Exception {
+ Path projectFolder = temp.resolve("browser-sqlite-project");
+ try (ActiveThingifierWorkspace source =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ source.replaceWithYaml(TestResources.text("/models/project-tasks.yaml"));
+ createRelatedProjectAndTodo(new DynamicThingifierApiProxy(source));
+ source.switchStorage(WorkspaceStorage.sqliteFile(temp.resolve("source.sqlite")));
+ new CrudUiController(source).saveProject(request(projectFolder));
+ }
+
+ try (ActiveThingifierWorkspace target =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ UiHttpResponse response =
+ new CrudUiController(target)
+ .loadProjectFiles(browserFilePayload(projectFolder, "browser-sqlite"));
+ DynamicThingifierApiProxy targetProxy = new DynamicThingifierApiProxy(target);
+
+ Assertions.assertEquals(200, response.statusCode(), response.body());
+ Assertions.assertEquals("sqlite-file", target.snapshot().storage().mode());
+ Assertions.assertEquals(
+ "Browser folder: browser-sqlite", target.snapshot().projectPath());
+ Assertions.assertTrue(targetProxy.getJson("projects").body().contains("Project A"));
+ Assertions.assertEquals(
+ 1,
+ root(targetProxy.getJson("projects/1/tasks").body())
+ .getAsJsonArray("todos")
+ .size());
+ }
+ }
+
+ @Test
+ public void invalidBrowserLoadFilesDoesNotMutateWorkspace() {
+ try (ActiveThingifierWorkspace workspace =
+ ActiveThingifierWorkspace.defaultTodoManagerWorkspace()) {
+ long version = workspace.snapshot().version();
+ List