From 0fd8c8a445232ba5b499b630c45e34eea288f23f Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Wed, 25 Oct 2023 11:41:12 +0200 Subject: [PATCH 01/21] test: ProjectExplorer view Component tests - Initial commit to add component tests to the ProjectExplorer view --- ui/tests/unit/views/ProjectExplorer.spec.ts | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 ui/tests/unit/views/ProjectExplorer.spec.ts diff --git a/ui/tests/unit/views/ProjectExplorer.spec.ts b/ui/tests/unit/views/ProjectExplorer.spec.ts new file mode 100644 index 000000000..af8b018e8 --- /dev/null +++ b/ui/tests/unit/views/ProjectExplorer.spec.ts @@ -0,0 +1,59 @@ +import { shallowMount, VueWrapper } from "@vue/test-utils"; +import ProjectsExplorer from "@/views/ProjectsExplorer.vue"; +import { createRouter, createWebHistory } from "vue-router"; +import * as _api from "@/api/api"; +import { ProjectsExplorerData } from "@/types/types"; + +const api = _api as any; + +jest.mock("@/api/api"); + +describe("ProjectsExplorer", () => { + const mock_routes = [ + { + path: "/", + redirect: "/item_a" + }, + { + path: "/item_a", + component: { + template: "Welcome to item a", + }, + }, + { + path: "/item_b", + component: { + template: "Welcome to item b", + }, + }, + { + path: "/item_c", + component: { + template: "Welcome to item c", + }, + }, + ]; + const router = createRouter({ + history: createWebHistory(), + routes: mock_routes, + }); + let wrapper: VueWrapper; + + beforeEach(function() { + const mockRouter = { + push: jest.fn(), + }; + + wrapper = shallowMount(ProjectsExplorer, { + global: { + plugins: [router], + mocks: { + $router: mockRouter, + }, + }, + }); + + }); + + +}) \ No newline at end of file From 0979f58910ea28a3e265057d7d980c8ed0215f37 Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Wed, 1 Nov 2023 14:45:20 +0100 Subject: [PATCH 02/21] Further progress --- ui/tests/unit/views/ProjectExplorer.spec.ts | 125 +++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/ui/tests/unit/views/ProjectExplorer.spec.ts b/ui/tests/unit/views/ProjectExplorer.spec.ts index af8b018e8..53bcb65cd 100644 --- a/ui/tests/unit/views/ProjectExplorer.spec.ts +++ b/ui/tests/unit/views/ProjectExplorer.spec.ts @@ -2,13 +2,15 @@ import { shallowMount, VueWrapper } from "@vue/test-utils"; import ProjectsExplorer from "@/views/ProjectsExplorer.vue"; import { createRouter, createWebHistory } from "vue-router"; import * as _api from "@/api/api"; -import { ProjectsExplorerData } from "@/types/types"; +import { ProjectsExplorerData, StringArray } from "@/types/types"; const api = _api as any; jest.mock("@/api/api"); describe("ProjectsExplorer", () => { + let testData: Array; + const mock_routes = [ { path: "/", @@ -37,6 +39,7 @@ describe("ProjectsExplorer", () => { history: createWebHistory(), routes: mock_routes, }); + let wrapper: VueWrapper; beforeEach(function() { @@ -44,6 +47,24 @@ describe("ProjectsExplorer", () => { push: jest.fn(), }; + router.currentRoute.value.params = { projectId: "some-project" }; + + testData = [ + "some-project/folder-a-one/file1.parquet", + "some-project/folder-a-one/file2.parquet", + "some-project/folder-a-one/fileb.parquet", + "some-project/folder-a-one/filea.parquet", + "some-project/folder-b-two/file1.parquet", + "some-project/folder-b-two/file2.parquet", + "some-project/folder-d-four/file2.parquet", + "some-project/folder-d-four/file1.parquet", + "some-project/folder-c-three/file1.parquet" + ]; + + api.getProject.mockImplementationOnce(() => { + return Promise.resolve(testData); + }); + wrapper = shallowMount(ProjectsExplorer, { global: { plugins: [router], @@ -55,5 +76,105 @@ describe("ProjectsExplorer", () => { }); + test("setting project content", () => { + expect(wrapper.vm.projectContent).toEqual( + { + "folder-a-one": [ + "file1.parquet", + "file2.parquet", + "fileb.parquet", + "filea.parquet" + ], + "folder-b-two": ["file1.parquet", "file2.parquet"], + "folder-d-four": ["file2.parquet", "file1.parquet"], + "folder-c-three": ["file1.parquet"] + } + ); + }); + + test("sorts folders", () => { + expect(wrapper.vm.getSortedFolders()).toEqual(["folder-a-one", "folder-b-two", "folder-c-three", "folder-d-four"]); + }); + + test("sorts files", () => { + wrapper.vm.selectedFolder = "folder-a-one"; + expect(wrapper.vm.getSortedFiles()).toEqual(["file1.parquet", "file2.parquet", "filea.parquet", "fileb.parquet"]); + wrapper.vm.selectedFolder = "folder-b-two"; + expect(wrapper.vm.getSortedFiles()).toEqual(["file1.parquet", "file2.parquet"]); + wrapper.vm.selectedFolder = "folder-c-three"; + expect(wrapper.vm.getSortedFiles()).toEqual(["file1.parquet"]); + wrapper.vm.selectedFolder = "folder-d-four"; + expect(wrapper.vm.getSortedFiles()).toEqual(["file1.parquet", "file2.parquet"]); + }); + + test("ask if preview is empty and setting empty", () => { + expect(wrapper.vm.askIfPreviewIsEmpty()).toBe(true); + wrapper.vm.filePreview = [{"some-file": "foobar"}]; + expect(wrapper.vm.askIfPreviewIsEmpty()).toBe(false); + wrapper.vm.clearFilePreview(); + expect(wrapper.vm.askIfPreviewIsEmpty()).toBe(true); + }); + + test("succesfully creating a new folder", () => { + expect(wrapper.vm.createNewFolder).toBe(false); + wrapper.vm.setCreateNewFolder(); + expect(wrapper.vm.createNewFolder).toBe(true); + wrapper.vm.newFolder = "FOLDER-e-five"; + wrapper.vm.addNewFolder(); + expect(wrapper.vm.getSortedFolders()).toContain("folder-e-five"); + }); + + test("error creating folder containing /", () => { + expect(wrapper.vm.errorMessage).toBe(""); + wrapper.vm.setCreateNewFolder(); + wrapper.vm.newFolder = "folder/five"; + wrapper.vm.addNewFolder(); + expect(wrapper.vm.errorMessage).toBe("Folder name cannot contain /"); + }); + + test("error creating folder containing /", () => { + expect(wrapper.vm.errorMessage).toBe(""); + wrapper.vm.setCreateNewFolder(); + wrapper.vm.addNewFolder(); + expect(wrapper.vm.errorMessage).toBe("Folder name cannot be empty"); + }); + + test("cancel creating a new folder", () => { + expect(wrapper.vm.createNewFolder).toBe(false); + expect(wrapper.vm.newFolder).toBe(""); + wrapper.vm.setCreateNewFolder(); + wrapper.vm.newFolder = "some-folder"; + expect(wrapper.vm.createNewFolder).toBe(true); + expect(wrapper.vm.newFolder).toBe("some-folder"); + wrapper.vm.cancelNewFolder(); + expect(wrapper.vm.createNewFolder).toBe(false); + expect(wrapper.vm.newFolder).toBe(""); + }); + + test("success message uploading a file", () => { + expect(wrapper.vm.successMessage).toBe(""); + wrapper.vm.onUploadSuccess({object: "folder-d-four", filename: "some-new-file.extension"}); + expect(wrapper.vm.successMessage).toBe("Successfully uploaded file [some-new-file.extension] into directory [folder-d-four] of project: [some-project]"); + }); + + test("non table type", () => { + expect(wrapper.vm.isNonTableType("some-file.parquet")).toBe(false); + expect(wrapper.vm.isNonTableType("some-file.csv.gz")).toBe(true); + expect(wrapper.vm.isNonTableType("some-file.rda")).toBe(true); + expect(wrapper.vm.isNonTableType("some-file")).toBe(true); + }); -}) \ No newline at end of file + // For some reason, this test does not like to work properly (something to do with a watcher causing an undefined error) + // test("setting and clearing file to delete", () => { + // expect(wrapper.vm.fileToDelete).toBe(""); + // expect(wrapper.vm.folderToDeleteFrom).toBe(""); + // wrapper.vm.selectedFile = "file1.parquet"; + // wrapper.vm.selectedFolder = "folder-d-four"; + // wrapper.vm.deleteSelectedFile(); + // expect(wrapper.vm.fileToDelete).toBe("file1.parquet"); + // expect(wrapper.vm.folderToDeleteFrom).toBe("folder-d-four"); + // wrapper.vm.clearRecordToDelete(); + // expect(wrapper.vm.fileToDelete).toBe(""); + // expect(wrapper.vm.folderToDeleteFrom).toBe(""); + // }); +}); From fb38307641e95e63cd5e082f9d485fba5d01e592 Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Thu, 2 Nov 2023 11:34:13 +0100 Subject: [PATCH 03/21] Finalized testing --- ui/tests/unit/views/ProjectExplorer.spec.ts | 92 ++++++++++++++++++--- 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/ui/tests/unit/views/ProjectExplorer.spec.ts b/ui/tests/unit/views/ProjectExplorer.spec.ts index 53bcb65cd..299169a1e 100644 --- a/ui/tests/unit/views/ProjectExplorer.spec.ts +++ b/ui/tests/unit/views/ProjectExplorer.spec.ts @@ -164,17 +164,83 @@ describe("ProjectsExplorer", () => { expect(wrapper.vm.isNonTableType("some-file")).toBe(true); }); - // For some reason, this test does not like to work properly (something to do with a watcher causing an undefined error) - // test("setting and clearing file to delete", () => { - // expect(wrapper.vm.fileToDelete).toBe(""); - // expect(wrapper.vm.folderToDeleteFrom).toBe(""); - // wrapper.vm.selectedFile = "file1.parquet"; - // wrapper.vm.selectedFolder = "folder-d-four"; - // wrapper.vm.deleteSelectedFile(); - // expect(wrapper.vm.fileToDelete).toBe("file1.parquet"); - // expect(wrapper.vm.folderToDeleteFrom).toBe("folder-d-four"); - // wrapper.vm.clearRecordToDelete(); - // expect(wrapper.vm.fileToDelete).toBe(""); - // expect(wrapper.vm.folderToDeleteFrom).toBe(""); - // }); + test("setting and clearing file to delete", () => { + expect(wrapper.vm.fileToDelete).toBe(""); + expect(wrapper.vm.folderToDeleteFrom).toBe(""); + // Important: selectedFolder before selectedFile, since watcher for selectedFolder resets selectedFile to "" + wrapper.vm.selectedFolder = "folder-d-four"; + wrapper.vm.selectedFile = "file1.parquet"; + wrapper.vm.deleteSelectedFile(); + expect(wrapper.vm.fileToDelete).toBe("file1.parquet"); + expect(wrapper.vm.folderToDeleteFrom).toBe("folder-d-four"); + wrapper.vm.clearRecordToDelete(); + expect(wrapper.vm.fileToDelete).toBe(""); + expect(wrapper.vm.folderToDeleteFrom).toBe(""); + }); + + test("successfully proceed deleting file", async () => { + api.deleteObject.mockImplementation(() => { + return Promise.resolve({}) + }); + wrapper.vm.selectedFolder = "folder-d-four"; + wrapper.vm.selectedFile = "file2.parquet"; + await wrapper.vm.proceedDelete("folder-d-four/file2.parquet"); + expect(wrapper.vm.selectedFile).toBe(""); + expect(wrapper.vm.successMessage).toBe("Successfully deleted file [file2.parquet] from directory [folder-d-four] of project: [some-project]"); + }); + + test("error proceed deleting file", async () => { + const error = new Error("fail"); + api.deleteObject.mockImplementation(() => { + return Promise.reject(error) + }); + wrapper.vm.selectedFolder = "doesnt-exist"; + wrapper.vm.selectedFile = "doesnot.exists"; + await wrapper.vm.proceedDelete("doesnt-exist/doesnot.exists"); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.errorMessage).toBe(error); + }); + + test("successfully reloads project", async () => { + let new_data = [ + "some-project/other-folder/one-some.file", + "some-project/other-folder/one-some.file" + ]; + expect(wrapper.vm.getSortedFolders()).toEqual(["folder-a-one", "folder-b-two", "folder-c-three", "folder-d-four"]); + api.getProject.mockImplementation(() => { + return Promise.resolve(new_data) + }); + await wrapper.vm.reloadProject(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.getSortedFolders()).toEqual(["other-folder"]); + }); + + test("error reloading project", async () => { + const error = new Error("fail"); + api.getProject.mockImplementation(() => { + return Promise.reject(error) + }); + await wrapper.vm.reloadProject(); + expect(wrapper.vm.errorMessage).toBe("Could not load project: Error: fail."); + }); + + test("reloads project with call to callback", async () => { + const someFunction = jest.fn(); + await wrapper.vm.reloadProject(someFunction); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(someFunction).toBeCalled(); + }); + + test("show error message with string", () => { + wrapper.vm.showErrorMessage("some very random error"); + expect(wrapper.vm.errorMessage).toBe("some very random error"); + }); + + test("show error message with Error", () => { + const error = new Error("fail"); + wrapper.vm.showErrorMessage(error); + expect(wrapper.vm.errorMessage).toBe(error); + }); }); From 1dd905fad75ceebea76eb199359b59d8b29c68ec Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Thu, 9 Nov 2023 09:26:10 +0100 Subject: [PATCH 04/21] - Fixed warning in test "show error message with Error" where an Error type is supplied but an string was expected (and the error comes from UploadFile, which should always be a string). - Changed the catch error message on ProceedDelete within ProjectsExplorer.vue to auto convert to string just in case. --- ui/src/views/ProjectsExplorer.vue | 2 +- ui/tests/unit/views/ProjectExplorer.spec.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ui/src/views/ProjectsExplorer.vue b/ui/src/views/ProjectsExplorer.vue index c0c381c70..50a789d5b 100644 --- a/ui/src/views/ProjectsExplorer.vue +++ b/ui/src/views/ProjectsExplorer.vue @@ -431,7 +431,7 @@ export default defineComponent({ this.successMessage = `Successfully deleted file [${file}] from directory [${folder}] of project: [${this.projectId}]`; }) .catch((error) => { - this.errorMessage = error; + this.errorMessage = `${error}`; }); }, reloadProject(callback: Function | undefined = undefined) { diff --git a/ui/tests/unit/views/ProjectExplorer.spec.ts b/ui/tests/unit/views/ProjectExplorer.spec.ts index 299169a1e..62dde1e93 100644 --- a/ui/tests/unit/views/ProjectExplorer.spec.ts +++ b/ui/tests/unit/views/ProjectExplorer.spec.ts @@ -198,7 +198,7 @@ describe("ProjectsExplorer", () => { wrapper.vm.selectedFile = "doesnot.exists"; await wrapper.vm.proceedDelete("doesnt-exist/doesnot.exists"); await wrapper.vm.$nextTick(); - expect(wrapper.vm.errorMessage).toBe(error); + expect(wrapper.vm.errorMessage).toBe(`${error}`); }); test("successfully reloads project", async () => { @@ -238,9 +238,10 @@ describe("ProjectsExplorer", () => { expect(wrapper.vm.errorMessage).toBe("some very random error"); }); - test("show error message with Error", () => { - const error = new Error("fail"); - wrapper.vm.showErrorMessage(error); - expect(wrapper.vm.errorMessage).toBe(error); + test("show error message used when UploadFile fails", () => { + // Since showErrorMessage already expects a string and is only used in FileUpload + // and FileUpload only returns the error as string, using only a string in this test. + wrapper.vm.showErrorMessage("A very random UploadFile error string"); + expect(wrapper.vm.errorMessage).toBe("A very random UploadFile error string"); }); }); From 5142b15fede7213774de490b2be03c52a289d4ba Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Thu, 9 Nov 2023 09:50:10 +0100 Subject: [PATCH 05/21] - Fixed a bug in SimpleTable test that would throw warnings about unused props "nCols" and "nRows" --- ui/tests/unit/components/SimpleTable.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/tests/unit/components/SimpleTable.spec.ts b/ui/tests/unit/components/SimpleTable.spec.ts index b73579e75..3ff40b8fa 100644 --- a/ui/tests/unit/components/SimpleTable.spec.ts +++ b/ui/tests/unit/components/SimpleTable.spec.ts @@ -72,6 +72,8 @@ describe("SimpleTable", () => { props: { data: data, maxWidth: 600, + nCols: 7, + nRows: data.length }, }); }); From 44e1fc71bef5bb61e9ae12e8ce3b7233b15461f0 Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Wed, 1 Nov 2023 16:14:59 +0100 Subject: [PATCH 06/21] Armadillo controller including autoload. --- scripts/ops/armadilloctl.zsh | 173 +++++++++++++++++++++++++++++++++++ scripts/ops/env.dist | 11 +++ 2 files changed, 184 insertions(+) create mode 100755 scripts/ops/armadilloctl.zsh create mode 100644 scripts/ops/env.dist diff --git a/scripts/ops/armadilloctl.zsh b/scripts/ops/armadilloctl.zsh new file mode 100755 index 000000000..6db3d53f8 --- /dev/null +++ b/scripts/ops/armadilloctl.zsh @@ -0,0 +1,173 @@ +#!/usr/bin/env zsh + +CURL_OPTS=--silent + +# curl -X 'GET' \ +# 'http://localhost:8080/ds-profiles' \ +# -H 'accept: application/json' \ +# -H 'Authorization: Basic YWRtaW46YWRtaW4=' + +function get_profiles() { + profile_names=$(curl $CURL_OPTS --user $CREDENTIALS --request 'GET' --header 'accept: application/json' $ARMADILLO_URL/ds-profiles | jq -r '.[] | "\(.name)"') +} + +function status { + if [ -z "$1" ]; then + echo "status needs profile name." + return 1 + fi + name=$1 + + stats=$(curl $CURL_OPTS --user "${CREDENTIALS}" --request 'GET' --header 'accept: application/json' "${ARMADILLO_URL}/ds-profiles/{$name}" | jq -r '"\(.name) = \(.container.status)"') + echo $stats +} + +function start { + if [ -z "$1" ]; then + echo "start needs profile name." + return 1 + fi + + profile=$1 + cmd="$ARMADILLO_URL/ds-profiles/$profile/start" + echo "Starting '$profile' on $cmd" + + curl --user $CREDENTIALS --request 'POST' $cmd --data '' +} + +function stop { + if [ -z "$1" ]; then + echo "stop needs profile name." + return 1 + fi + + profile=$1 + cmd="$ARMADILLO_URL/ds-profiles/$profile/stop" + echo "Stopping '$profile' on $cmd" + + curl --user $CREDENTIALS --request 'POST' $cmd --data '' +} + +function restart { + if [ -z "$1" ]; then + echo "restart needs profile name." + return 1 + fi + + profile=$1 + echo "Restarting $profile" + stop "$profile" + sleep 5 + start "$profile" + sleep 5 + status "$profile" + +} + +function is_auto_start() { + if [[ $ARMADILLO_PROFILES_AUTOSTART =~ (^|[[:space:]])$1($|[[:space:]]) ]] + then + return 0 + else + return 1 + fi +} + +function doAll { + command=$1 + get_profiles + + echo "${profile_names}" | while read -r item; do + "$command" "${item}" + done +} + +function statusAll() { + doAll status +} + +function startAll() { + doAll start +} + +function stopAll() { + doAll stop +} + +function restartAll { + doAll stop + sleep 5 + doAll start +} + +function autoStart() { + get_profiles + echo "Auto starting ..." + echo "${profile_names}" | while read -r item + do + if is_auto_start "${item}" + then + start "${item}" + fi + done +} + +function check_dependencies() { + if ! which jq > /dev/null ; then + echo "Please install jq for json parsing ... exiting" + exit 1 + fi +} + +function var_found() { + echo "Variable $1 found" +} + +function var_empty() { + echo "Variable $1 not set! ... exiting" + exit 1 +} + +function all_set() { + [[ -n "$ARMADILLO_URL" ]] && var_found ARMADILLO_URL + [[ -z "$ARMADILLO_URL" ]] && var_empty ARMADILLO_URL + + [[ -n "$ARMADILLO_ADMIN_USER" ]] && var_found ARMADILLO_ADMIN_USER + [[ -z "$ARMADILLO_ADMIN_USER" ]] && var_empty ARMADILLO_ADMIN_USER + + [[ -n "$ARMADILLO_ADMIN_PASSWORD" ]] && var_found ARMADILLO_ADMIN_PASSWORD + [[ -z "$ARMADILLO_ADMIN_PASSWORD" ]] && var_empty ARMADILLO_ADMIN_PASSWORD + + # Set to all if not set with value. + [[ -n "${ARMADILLO_PROFILES_AUTOSTART}" ]] && var_found ARMADILLO_PROFILES_AUTOSTART + [[ -z "${ARMADILLO_PROFILES_AUTOSTART}" ]] && ARMADILLO_PROFILES_AUTOSTART="__ALL__" + + CREDENTIALS="${ARMADILLO_ADMIN_USER}:${ARMADILLO_ADMIN_PASSWORD}" + + echo "Armadillo settings:" + echo " URL : ${ARMADILLO_URL}" + echo " ADMIN_USER : ${ARMADILLO_ADMIN_USER}" + echo " PROFILES_AUTOSTART : ${ARMADILLO_PROFILES_AUTOSTART}" +} + +check_dependencies || exit + +# FIXME: loads from local files +if [[ -f ".env" ]] +then + echo "Sourcing .env" + source ".env" +else + echo "Sourcing dev.env" + source "dev.env" +fi + +all_set + +get_profiles + +if [[ "$1" =~ ^(status|start|stop|restart|statusAll|startAll|stopAll|restartAll|autoStart)$ ]]; then + "$@" +else + echo "Please provide one of the following argument: status | start | stop | restart | statusAll | startAll | stopAll | restartAll | autoStart" +fi diff --git a/scripts/ops/env.dist b/scripts/ops/env.dist new file mode 100644 index 000000000..0a212e9a1 --- /dev/null +++ b/scripts/ops/env.dist @@ -0,0 +1,11 @@ +# copy to .env + +# FIXME: We could / should test for admin user in overload config file instead? +# ARMADILLO_YML_FILE= + +ARMADILLO_URL=http://localhost:8080 +ARMADILLO_ADMIN_USER=admin +ARMADILLO_ADMIN_PASSWORD=admin + +# Space separated values between double quotes. Leaving empty falls back to all profiles. +ARMADILLO_PROFILES_AUTOSTART="default rock" \ No newline at end of file From 41710f9374bd4ec1c11fe6a60b09f0647bd40518 Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Thu, 2 Nov 2023 12:08:22 +0100 Subject: [PATCH 07/21] Make sure to export vars. --- scripts/ops/env.dist | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/ops/env.dist b/scripts/ops/env.dist index 0a212e9a1..7b19ccef4 100644 --- a/scripts/ops/env.dist +++ b/scripts/ops/env.dist @@ -1,11 +1,11 @@ -# copy to .env +# copy this file to ie "/etc/armadillo/acc.env" # FIXME: We could / should test for admin user in overload config file instead? # ARMADILLO_YML_FILE= -ARMADILLO_URL=http://localhost:8080 -ARMADILLO_ADMIN_USER=admin -ARMADILLO_ADMIN_PASSWORD=admin +export ARMADILLO_URL=http://localhost:8080 +export ARMADILLO_ADMIN_USER=admin +export ARMADILLO_ADMIN_PASSWORD=admin # Space separated values between double quotes. Leaving empty falls back to all profiles. -ARMADILLO_PROFILES_AUTOSTART="default rock" \ No newline at end of file +export ARMADILLO_PROFILES_AUTOSTART="default rock" From 238f87ff9167480ee710f81fe25b1240493d292a Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Thu, 2 Nov 2023 12:09:34 +0100 Subject: [PATCH 08/21] Cleanup controller script. --- scripts/ops/armadilloctl.zsh | 65 +++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/scripts/ops/armadilloctl.zsh b/scripts/ops/armadilloctl.zsh index 6db3d53f8..fd2acf7b5 100755 --- a/scripts/ops/armadilloctl.zsh +++ b/scripts/ops/armadilloctl.zsh @@ -2,56 +2,67 @@ CURL_OPTS=--silent -# curl -X 'GET' \ -# 'http://localhost:8080/ds-profiles' \ -# -H 'accept: application/json' \ -# -H 'Authorization: Basic YWRtaW46YWRtaW4=' +WARN="\033[33m" +ERR="\033[31m" +B="\033[0m" + +function warning() { + echo -e "WARNING: ${WARN}${1}${B}" +} + +function error() { + echo -e "ERROR: ${ERR}${1}${B}" + exit 1 +} + +function is_armadillo_running() { + cmd="${ARMADILLO_URL}" + curl $CURL_OPTS --request "GET" "${cmd}" > /dev/null || (error "Armadillo not running."; exit 1) +} function get_profiles() { - profile_names=$(curl $CURL_OPTS --user $CREDENTIALS --request 'GET' --header 'accept: application/json' $ARMADILLO_URL/ds-profiles | jq -r '.[] | "\(.name)"') + cmd="$ARMADILLO_URL/ds-profiles" + profile_names=$(curl $CURL_OPTS --user "${CREDENTIALS}" --request "GET" --header "accept: application/json" "${cmd}" | jq -r '.[] | "\(.name)"') } function status { if [ -z "$1" ]; then - echo "status needs profile name." - return 1 + error "$0 needs profile name. Or try ${0}All" fi name=$1 - stats=$(curl $CURL_OPTS --user "${CREDENTIALS}" --request 'GET' --header 'accept: application/json' "${ARMADILLO_URL}/ds-profiles/{$name}" | jq -r '"\(.name) = \(.container.status)"') + cmd="${ARMADILLO_URL}/ds-profiles/{$name}" + stats=$(curl $CURL_OPTS --user "${CREDENTIALS}" --request "GET" --header "accept: application/json" "${cmd}" | jq -r '"\(.name) = \(.container.status)"') echo $stats } function start { if [ -z "$1" ]; then - echo "start needs profile name." - return 1 + error "$0 needs profile name. Or try ${0}All" fi - profile=$1 + profile="$1" cmd="$ARMADILLO_URL/ds-profiles/$profile/start" echo "Starting '$profile' on $cmd" - curl --user $CREDENTIALS --request 'POST' $cmd --data '' + curl $CURL_OPTS --user $CREDENTIALS --request "POST" "$cmd" --data "" } function stop { if [ -z "$1" ]; then - echo "stop needs profile name." - return 1 + error "$0 needs profile name. Or try ${0}All" fi - profile=$1 + profile="$1" cmd="$ARMADILLO_URL/ds-profiles/$profile/stop" echo "Stopping '$profile' on $cmd" - curl --user $CREDENTIALS --request 'POST' $cmd --data '' + curl $CURL_OPTS --user $CREDENTIALS --request "POST" $cmd --data "" } function restart { if [ -z "$1" ]; then - echo "restart needs profile name." - return 1 + error "$0 needs profile name. Or try ${0}All" fi profile=$1 @@ -120,7 +131,7 @@ function check_dependencies() { } function var_found() { - echo "Variable $1 found" + echo "Variable $1 found ..." } function var_empty() { @@ -144,7 +155,7 @@ function all_set() { CREDENTIALS="${ARMADILLO_ADMIN_USER}:${ARMADILLO_ADMIN_PASSWORD}" - echo "Armadillo settings:" + echo "\nArmadillo settings:" echo " URL : ${ARMADILLO_URL}" echo " ADMIN_USER : ${ARMADILLO_ADMIN_USER}" echo " PROFILES_AUTOSTART : ${ARMADILLO_PROFILES_AUTOSTART}" @@ -152,22 +163,14 @@ function all_set() { check_dependencies || exit -# FIXME: loads from local files -if [[ -f ".env" ]] -then - echo "Sourcing .env" - source ".env" -else - echo "Sourcing dev.env" - source "dev.env" -fi +all_set || exit -all_set +is_armadillo_running || exit get_profiles if [[ "$1" =~ ^(status|start|stop|restart|statusAll|startAll|stopAll|restartAll|autoStart)$ ]]; then "$@" else - echo "Please provide one of the following argument: status | start | stop | restart | statusAll | startAll | stopAll | restartAll | autoStart" + echo "\nPlease provide one of the following argument: status | start | stop | restart | statusAll | startAll | stopAll | restartAll | autoStart" fi From ccced184002cc42ea35740d2bc635dc593f73db2 Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Thu, 2 Nov 2023 12:18:46 +0100 Subject: [PATCH 09/21] Add docs. --- scripts/ops/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scripts/ops/README.md diff --git a/scripts/ops/README.md b/scripts/ops/README.md new file mode 100644 index 000000000..f6722f2d6 --- /dev/null +++ b/scripts/ops/README.md @@ -0,0 +1,30 @@ +# Armadillo for OPS + +To monitor and control Armadillo and its DataShield docker containers we need to use the Armadillo API. + +## Armadillo Controller + +With `armadilloctl` you can control the DataShield docker containers. + +To make this possible Armadillo must have an admin user set. + +### env.dist + +The file [`env.dist`](./env.dist) lists the required environment variables. + +- Copy this over to a location of choice ie `conf/armadillo.acc.env` + +### run + +Make sure to `source` the file in your current `zsh` or through a new shell. + +```zsh +zsh -c "source conf/armadillo.acc.env ; ./armadilloctl.zsh" +``` + +### examples + +- `./armadillo.zsh statusAll` +- `./armadillo.zsh autoStart` +- `./armadillo.zsh stop default` +- `./armadillo.zsh startAll` \ No newline at end of file From 1f452992cc9866f979bc9b002b1b9f8fa3ee0db8 Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Thu, 2 Nov 2023 12:19:19 +0100 Subject: [PATCH 10/21] Add test script (wip) --- scripts/ops/test_armadilloctl.zsh | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100755 scripts/ops/test_armadilloctl.zsh diff --git a/scripts/ops/test_armadilloctl.zsh b/scripts/ops/test_armadilloctl.zsh new file mode 100755 index 000000000..9d480fe33 --- /dev/null +++ b/scripts/ops/test_armadilloctl.zsh @@ -0,0 +1,67 @@ +#!/usr/bin/env zsh + +OUT=/dev/stdout +if [ -z "$1" ]; then + OUT="/dev/null" +fi + +function usage() { + echo 'zsh -c "source dev.env ; ./test_armadilloctl.zsh" || echo FAILED' +} + +function colors() { + for c in {0..255}; do + printf "\033[48;5;%sm%3d\033[0m " "$c" "$c" + if (( c == 15 )) || (( c > 15 )) && (( (c-15) % 6 == 0 )); then + printf "\n" + fi + done +} + +OK="\033[32m" +WARN="\033[33m" +ERR="\033[31m" +B="\033[0m" + +function error() { + echo -e "TEST:: ERROR: ${ERR}${1}${B}" +} + +function warning() { + echo -e "TEST:: WARNING: ${WARN}${1}${B}" +} + +function success() { + echo -e "TEST:: SUCCES: ${OK}${1}${B}" +} + + +function failed() { + warn $0 +} +function do_sleep() { + warning "Pausing for $1 seconds before '$2'." + sleep "$1" + echo "..." +} + +error "NOTE: controller exit values are incomplete ... needs eyes" +warning "Make sure Armadillo is running" + +./armadilloctl.zsh > $OUT || success "Needs a command." + +./armadilloctl.zsh status > $OUT || success "status needs profile name" +./armadilloctl.zsh statusAll > $OUT && success "statusAll needs running Armadillo" + +./armadilloctl.zsh stopAll > $OUT && success "stopAll needs running Armadillo" + +do_sleep 5 "autoStart" +./armadilloctl.zsh autoStart > $OUT && success "autoStart needs running Armadillo" + +do_sleep 5 "startAll" +./armadilloctl.zsh startAll > $OUT && success "startAll needs running Armadillo" + +do_sleep 5 "restartAll" +./armadilloctl.zsh restartAll > $OUT && success "restartAll needs running Armadillo" + +# colors From ae602cd17aa18cc48776d20050f01399058b0c49 Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Thu, 2 Nov 2023 12:23:04 +0100 Subject: [PATCH 11/21] Add test script ref in docs. --- scripts/ops/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/ops/README.md b/scripts/ops/README.md index f6722f2d6..4f1a274be 100644 --- a/scripts/ops/README.md +++ b/scripts/ops/README.md @@ -27,4 +27,8 @@ zsh -c "source conf/armadillo.acc.env ; ./armadilloctl.zsh" - `./armadillo.zsh statusAll` - `./armadillo.zsh autoStart` - `./armadillo.zsh stop default` -- `./armadillo.zsh startAll` \ No newline at end of file +- `./armadillo.zsh startAll` + +### Test script (WIP) + +You can test `armadilloctl.zsh` using `test_armadilloctl.zsh`. From 599414da18d6a1b4b6bccb540e4323586abf4f2d Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Thu, 2 Nov 2023 13:28:03 +0100 Subject: [PATCH 12/21] Fixes #531: Add how to run test. --- scripts/ops/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/ops/README.md b/scripts/ops/README.md index 4f1a274be..96356c733 100644 --- a/scripts/ops/README.md +++ b/scripts/ops/README.md @@ -32,3 +32,7 @@ zsh -c "source conf/armadillo.acc.env ; ./armadilloctl.zsh" ### Test script (WIP) You can test `armadilloctl.zsh` using `test_armadilloctl.zsh`. + +```zsh +zsh -c "source dev.env ; ./test_armadilloctl.zsh" || echo FAILED +``` From 3f05b805ae4c28205e0fae46f2018c3e95e52d67 Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Wed, 27 Sep 2023 16:06:20 +0200 Subject: [PATCH 13/21] test: Add Component Profiles View test - Started on getting component test for the Profiles View integrated into standardized testing. --- ui/tests/unit/views/Profiles.spec.ts | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 ui/tests/unit/views/Profiles.spec.ts diff --git a/ui/tests/unit/views/Profiles.spec.ts b/ui/tests/unit/views/Profiles.spec.ts new file mode 100644 index 000000000..857c9381c --- /dev/null +++ b/ui/tests/unit/views/Profiles.spec.ts @@ -0,0 +1,49 @@ +import { shallowMount, VueWrapper } from "@vue/test-utils"; +import Profiles from "@/views/Profiles.vue"; +import { createRouter, createWebHistory } from "vue-router"; +import * as _api from "@/api/api"; +import { Profile } from "@/types/api" + +const api = _api as any; + +jest.mock("@/api/api") + +describe("Profiles", () => { + let testData: Profile[]; + + let profileToAdd: Profile; + let profileToEdit: Profile; + + const mock_routes = [ + { + path: "/", + redirect: "/item_a", + }, + { + path: "/item_a", + component: { + template: "Welcome to item a", + }, + }, + { + path: "/item_b", + component: { + template: "Welcome to item b", + }, + }, + { + path: "/item_c", + component: { + template: "Welcome to item c", + }, + }, + ]; + const router = createRouter({ + history: createWebHistory(), + routes: mock_routes + }) + let wrapper: VueWrapper; + + +} +) From 230e807f1edb95d353fe589c751b22910094c233 Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Fri, 29 Sep 2023 08:26:01 +0200 Subject: [PATCH 14/21] Added tests for Profiles Removed duplicate test for Users.spec.ts --- ui/tests/unit/views/Profiles.spec.ts | 162 ++++++++++++++++++++++++++- ui/tests/unit/views/Users.spec.ts | 16 --- 2 files changed, 158 insertions(+), 20 deletions(-) diff --git a/ui/tests/unit/views/Profiles.spec.ts b/ui/tests/unit/views/Profiles.spec.ts index 857c9381c..9eef943d3 100644 --- a/ui/tests/unit/views/Profiles.spec.ts +++ b/ui/tests/unit/views/Profiles.spec.ts @@ -3,10 +3,11 @@ import Profiles from "@/views/Profiles.vue"; import { createRouter, createWebHistory } from "vue-router"; import * as _api from "@/api/api"; import { Profile } from "@/types/api" +import { processErrorMessages } from "@/helpers/errorProcessing"; const api = _api as any; -jest.mock("@/api/api") +jest.mock("@/api/api"); describe("Profiles", () => { let testData: Profile[]; @@ -44,6 +45,159 @@ describe("Profiles", () => { }) let wrapper: VueWrapper; - -} -) + beforeEach(function() { + const mockRouter = { + push: jest.fn(), + }; + + testData = [ + { + name: "default", + image: "datashield/armadillo-rserver", + host: "localhost", + port: 6311, + packageWhitelist: [ + "dsBase" + ], + functionBlacklist: [], + datashieldSeed: "100000000", + options: { + "datashield.seed": "100000000" + }, + container: { + tags: [ + "datashield/armadillo-rserver:2.0.0", + "datashield/armadillo-rserver:latest" + ], + status: "NOT_RUNNING" + } + }, + { + name: "profile1", + image: "source/some_profile1", + host: "localhost", + port: 6312, + packageWhitelist: [ + "dsBase" + ], + functionBlacklist: [], + datashieldSeed: "100000001", + options: { + "datashield.seed": "100000001" + }, + container: { + tags: ["source/some_profile1"], + status: "NOT_RUNNING" + } + } + ] + + api.getProfiles.mockImplementationOnce(() => { + return Promise.resolve(testData); + }); + + profileToAdd = { + name: "profile2", + image: "other_source/profile2", + host: "localhost", + port: 6313, + packageWhitelist: [ + "dsBase" + ], + functionBlacklist: [], + datashieldSeed: "100000002", + options: { + "datashield.seed": "100000002" + }, + container: { + tags: ["other_source/profile2"], + status: "NOT_FOUND" + } + } + + wrapper = shallowMount(Profiles, { + global: { + plugins: [router], + mocks: { + $router: mockRouter, + }, + }, + }); + }); + test("clears updated profile index and name", () => { + wrapper.vm.profileToEditIndex = 2; + wrapper.vm.profileToEdit = "foobar" + wrapper.vm.clearProfileToEdit(); + expect(wrapper.vm.profileToEditIndex).toBe(-1); + expect(wrapper.vm.profileToEdit).toBe(""); + }); + + test("clears user messages", () => { + wrapper.vm.successMessage = "testSuccess"; + wrapper.vm.errorMessage = "testError"; + wrapper.vm.clearUserMessages(); + expect(wrapper.vm.successMessage).toBe(""); + expect(wrapper.vm.errorMessage).toBe(""); + }); + + test("clears new profile", () => { + wrapper.vm.addProfile = true; + wrapper.vm.profiles.unshift(profileToAdd); + wrapper.vm.profileToEditIndex = 0; + wrapper.vm.clearProfileToEdit(); + expect(wrapper.vm.addProfile).toBe(false); + expect(wrapper.vm.profileToEditIndex).toBe(-1); + expect(wrapper.vm.profileToEdit).toBe(""); + }); + + test("new datashield seed", () => { + expect(wrapper.vm.firstFreeSeed).toBe("100000002"); + }); + + test("new profile port", () => { + expect(wrapper.vm.firstFreePort).toBe(6313); + }); + + test("edits profile", () => { + wrapper.vm.profileToEdit = ""; + wrapper.vm.editProfile(profileToAdd); + expect(wrapper.vm.profileToEdit).toBe(profileToAdd.name); + }); + + test("retrieve index of profile to edit", () => { + wrapper.vm.profileToEdit = "profile1"; + const index = wrapper.vm.getEditIndex(); + expect(index).toBe(1); + }); + + test("reloads profiles", async () => { + const testFunction = jest.fn() + const updatedProfiles = testData.concat([profileToAdd]) + api.getProfiles.mockImplementation(() => { + testFunction(); + return Promise.resolve(updatedProfiles); + }); + wrapper.vm.reloadProfiles(); + expect(wrapper.vm.loading).toBe(true); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.loading).toBe(false); + expect(testFunction).toHaveBeenCalled(); + }); + + test("fail to rename default profile", () => { + wrapper.vm.profiles.unshift(profileToAdd); + wrapper.vm.profileToEditIndex = 0; + wrapper.vm.profileToEdit = "default" + wrapper.vm.saveEditedProfile(); + expect(wrapper.vm.errorMessage).toBe("Save failed: cannot rename 'default' package."); + }); + + test("fail to save a unnamed profile", () => { + wrapper.vm.addNewProfile(); + wrapper.vm.saveEditedProfile(); + expect(wrapper.vm.errorMessage).toBe("Cannot create profile with empty name."); + }); +}); diff --git a/ui/tests/unit/views/Users.spec.ts b/ui/tests/unit/views/Users.spec.ts index 6e7b5d807..752f31c03 100644 --- a/ui/tests/unit/views/Users.spec.ts +++ b/ui/tests/unit/views/Users.spec.ts @@ -210,22 +210,6 @@ describe("Users", () => { expect(testFunction).toHaveBeenCalled(); }); - test("reloads users", async () => { - const testFunction = jest.fn(); - const updatedUsers = testData.concat([userToAdd]); - api.getUsers.mockImplementation(() => { - testFunction(); - return Promise.resolve(updatedUsers); - }); - wrapper.vm.reloadUsers(); - expect(wrapper.vm.loading).toBe(true); - await wrapper.vm.$nextTick(); - await wrapper.vm.$nextTick(); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.loading).toBe(false); - expect(testFunction).toHaveBeenCalled(); - }); - test("returns error when loading users fails", async () => { const error = new Error("fail"); api.getUsers.mockImplementation(() => { From 2e7d2229f1a30da1e776cafcab5b1ffbd3f51d72 Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Wed, 4 Oct 2023 13:30:05 +0200 Subject: [PATCH 15/21] Further progression. Stopping and starting works very poorly --- ui/tests/unit/views/Profiles.spec.ts | 102 +++++++++++++++++++++------ 1 file changed, 80 insertions(+), 22 deletions(-) diff --git a/ui/tests/unit/views/Profiles.spec.ts b/ui/tests/unit/views/Profiles.spec.ts index 9eef943d3..21b792f47 100644 --- a/ui/tests/unit/views/Profiles.spec.ts +++ b/ui/tests/unit/views/Profiles.spec.ts @@ -50,28 +50,51 @@ describe("Profiles", () => { push: jest.fn(), }; - testData = [ - { - name: "default", - image: "datashield/armadillo-rserver", - host: "localhost", - port: 6311, - packageWhitelist: [ - "dsBase" - ], - functionBlacklist: [], - datashieldSeed: "100000000", - options: { - "datashield.seed": "100000000" - }, - container: { - tags: [ - "datashield/armadillo-rserver:2.0.0", - "datashield/armadillo-rserver:latest" - ], - status: "NOT_RUNNING" - } + let default_profile_not_running = { + name: "default", + image: "datashield/armadillo-rserver", + host: "localhost", + port: 6311, + packageWhitelist: [ + "dsBase" + ], + functionBlacklist: [], + datashieldSeed: "100000000", + options: { + "datashield.seed": "100000000" }, + container: { + tags: [ + "datashield/armadillo-rserver:2.0.0", + "datashield/armadillo-rserver:latest" + ], + status: "NOT_RUNNING" + } + } + + let default_profile_running = { + name: "default", + image: "datashield/armadillo-rserver", + host: "localhost", + port: 6311, + packageWhitelist: [ + "dsBase" + ], + functionBlacklist: [], + datashieldSeed: "100000000", + options: { + "datashield.seed": "100000000" + }, + container: { + tags: [ + "datashield/armadillo-rserver:2.0.0", + "datashield/armadillo-rserver:latest" + ], + status: "RUNNING" + } + } + + testData = [default_profile_not_running].concat([ { name: "profile1", image: "source/some_profile1", @@ -90,11 +113,17 @@ describe("Profiles", () => { status: "NOT_RUNNING" } } - ] + ]); api.getProfiles.mockImplementationOnce(() => { return Promise.resolve(testData); }); + api.startProfile.mockImplementationOnce(() => { + return Promise.resolve(default_profile_running); + }); + api.stopProfile.mockImplementationOnce(() => { + return Promise.resolve(default_profile_not_running); + }); profileToAdd = { name: "profile2", @@ -187,6 +216,17 @@ describe("Profiles", () => { expect(testFunction).toHaveBeenCalled(); }); + test("returns error when loading profiles fails", async () => { + const error = new Error("fail"); + api.getProfiles.mockImplementation(() => { + return Promise.reject(error); + }); + wrapper.vm.reloadProfiles(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.errorMessage).toBe(`Could not load profiles: ${error}.`); + }); + test("fail to rename default profile", () => { wrapper.vm.profiles.unshift(profileToAdd); wrapper.vm.profileToEditIndex = 0; @@ -200,4 +240,22 @@ describe("Profiles", () => { wrapper.vm.saveEditedProfile(); expect(wrapper.vm.errorMessage).toBe("Cannot create profile with empty name."); }); + + test("starting default profile", () => { + wrapper.vm.startProfile("default"); + wrapper.vm.profileToEdit = "default"; + expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].name).toBe("default"); + expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].container.status).toBe("RUNNING"); + }); + + test("stopping default profile", () => { + wrapper.vm.startProfile("default"); + wrapper.vm.profileToEdit = "default"; + wrapper.vm.$nextTick(); + wrapper.vm.$nextTick(); + expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].name).toBe("default"); + expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].container.status).toBe("RUNNING"); + wrapper.vm.stopProfile("default"); + expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].container.status).toBe("NOT_RUNNING"); + }); }); From e20357c31ff1e8ccbe047b13b295f060d6993a6d Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Mon, 9 Oct 2023 15:44:06 +0200 Subject: [PATCH 16/21] Finalized tests for Profiles view starting and stopping profiles --- ui/tests/unit/views/Profiles.spec.ts | 84 ++++++++++++++++------------ 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/ui/tests/unit/views/Profiles.spec.ts b/ui/tests/unit/views/Profiles.spec.ts index 21b792f47..fdfe3a4f8 100644 --- a/ui/tests/unit/views/Profiles.spec.ts +++ b/ui/tests/unit/views/Profiles.spec.ts @@ -11,9 +11,11 @@ jest.mock("@/api/api"); describe("Profiles", () => { let testData: Profile[]; + let singleTestData: Profile[]; let profileToAdd: Profile; - let profileToEdit: Profile; + let default_profile_running: Profile; + let default_profile_not_running: Profile; const mock_routes = [ { @@ -50,7 +52,7 @@ describe("Profiles", () => { push: jest.fn(), }; - let default_profile_not_running = { + default_profile_not_running = { name: "default", image: "datashield/armadillo-rserver", host: "localhost", @@ -72,29 +74,10 @@ describe("Profiles", () => { } } - let default_profile_running = { - name: "default", - image: "datashield/armadillo-rserver", - host: "localhost", - port: 6311, - packageWhitelist: [ - "dsBase" - ], - functionBlacklist: [], - datashieldSeed: "100000000", - options: { - "datashield.seed": "100000000" - }, - container: { - tags: [ - "datashield/armadillo-rserver:2.0.0", - "datashield/armadillo-rserver:latest" - ], - status: "RUNNING" - } - } + default_profile_running = JSON.parse(JSON.stringify(default_profile_not_running)) + default_profile_running.container.status = "RUNNING" - testData = [default_profile_not_running].concat([ + singleTestData = [ { name: "profile1", image: "source/some_profile1", @@ -113,17 +96,13 @@ describe("Profiles", () => { status: "NOT_RUNNING" } } - ]); + ]; + + testData = [default_profile_not_running].concat(singleTestData); api.getProfiles.mockImplementationOnce(() => { return Promise.resolve(testData); }); - api.startProfile.mockImplementationOnce(() => { - return Promise.resolve(default_profile_running); - }); - api.stopProfile.mockImplementationOnce(() => { - return Promise.resolve(default_profile_not_running); - }); profileToAdd = { name: "profile2", @@ -241,21 +220,56 @@ describe("Profiles", () => { expect(wrapper.vm.errorMessage).toBe("Cannot create profile with empty name."); }); - test("starting default profile", () => { + test("starting default profile", async () => { + api.startProfile.mockImplementationOnce(() => { + return Promise.resolve(default_profile_running) + }); + api.getProfiles.mockImplementation(() => { + return Promise.resolve([default_profile_running].concat(singleTestData)) + }); wrapper.vm.startProfile("default"); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.successMessage).toBe("[default] was successfully started."); + expect(wrapper.vm.errorMessage).toBe(""); wrapper.vm.profileToEdit = "default"; + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].name).toBe("default"); expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].container.status).toBe("RUNNING"); }); - test("stopping default profile", () => { + test("stopping default profile", async () => { + api.startProfile.mockImplementationOnce(() => { + return Promise.resolve(default_profile_running) + }) + api.stopProfile.mockImplementationOnce(() => { + return Promise.resolve(default_profile_not_running) + }); + api.getProfiles.mockImplementation(() => { + return Promise.resolve([default_profile_running].concat(singleTestData)) + }); wrapper.vm.startProfile("default"); + await wrapper.vm.$nextTick() + expect(wrapper.vm.successMessage).toBe("[default] was successfully started."); + expect(wrapper.vm.errorMessage).toBe(""); wrapper.vm.profileToEdit = "default"; - wrapper.vm.$nextTick(); - wrapper.vm.$nextTick(); + wrapper.vm.reloadProfiles(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].name).toBe("default"); expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].container.status).toBe("RUNNING"); + api.getProfiles.mockImplementation(() => { + return Promise.resolve([default_profile_not_running].concat(singleTestData)) + }); wrapper.vm.stopProfile("default"); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.successMessage).toBe("[default] was successfully stopped."); + expect(wrapper.vm.errorMessage).toBe(""); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].container.status).toBe("NOT_RUNNING"); }); }); From af556c4ae3e8b9dcc0d6fcdbaa0397ae096d16c3 Mon Sep 17 00:00:00 2001 From: Clemens Tolboom Date: Thu, 12 Oct 2023 09:35:03 +0200 Subject: [PATCH 17/21] Test on duplicate ports. --- ui/src/views/Profiles.vue | 9 +++++++++ ui/tests/unit/views/Profiles.spec.ts | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/ui/src/views/Profiles.vue b/ui/src/views/Profiles.vue index e7bdbe735..f2dd5cc28 100644 --- a/ui/src/views/Profiles.vue +++ b/ui/src/views/Profiles.vue @@ -285,6 +285,15 @@ export default defineComponent({ const profileNames = this.profiles.map((profile) => { return profile.name; }); + + const portAlreadyUsed = this.profiles.some((prof) => { + return prof !== profile && prof.port == profile.port; + }); + if (portAlreadyUsed) { + this.errorMessage = `Save failed: port number [${profile.port}] already used.`; + return; + } + if ( this.profileToEdit === "default" && profile.name != this.profileToEdit diff --git a/ui/tests/unit/views/Profiles.spec.ts b/ui/tests/unit/views/Profiles.spec.ts index fdfe3a4f8..f0d976e99 100644 --- a/ui/tests/unit/views/Profiles.spec.ts +++ b/ui/tests/unit/views/Profiles.spec.ts @@ -214,6 +214,16 @@ describe("Profiles", () => { expect(wrapper.vm.errorMessage).toBe("Save failed: cannot rename 'default' package."); }); + test("fail to use same port for profile", () => { + wrapper.vm.profiles.unshift(profileToAdd); + let p = {... profileToAdd}; + p.port = 6313; + wrapper.vm.profiles.unshift(p); + wrapper.vm.profileToEditIndex = 0; + wrapper.vm.saveEditedProfile(); + expect(wrapper.vm.errorMessage).toBe("Save failed: port number [6313] already used."); + }); + test("fail to save a unnamed profile", () => { wrapper.vm.addNewProfile(); wrapper.vm.saveEditedProfile(); From 8e57324185fc785e92de805c97ecf4e883a513ba Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Thu, 12 Oct 2023 11:55:12 +0200 Subject: [PATCH 18/21] Revert some of the "fixed some VS code errors" due to "datashield.seed" showing up in menu for creating new profiles. --- ui/src/types/api.d.ts | 2 +- ui/src/views/Profiles.vue | 2 +- ui/tests/unit/views/Profiles.spec.ts | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/types/api.d.ts b/ui/src/types/api.d.ts index 9c1741d18..235da14ae 100644 --- a/ui/src/types/api.d.ts +++ b/ui/src/types/api.d.ts @@ -50,7 +50,7 @@ export type Profile = { functionBlacklist: StringArray; datashieldSeed: string; options: { - "datashield.seed": string; + "datashield.seed"?: string; }; container: { tags: StringArray; diff --git a/ui/src/views/Profiles.vue b/ui/src/views/Profiles.vue index f2dd5cc28..7f46b96e1 100644 --- a/ui/src/views/Profiles.vue +++ b/ui/src/views/Profiles.vue @@ -370,7 +370,7 @@ export default defineComponent({ packageWhitelist: ["dsBase"], functionBlacklist: [], datashieldSeed: this.firstFreeSeed, - options: { "datashield.seed": "" }, + options: {}, container: { tags: [], status: "unknown" }, }); this.profileToEditIndex = 0; diff --git a/ui/tests/unit/views/Profiles.spec.ts b/ui/tests/unit/views/Profiles.spec.ts index f0d976e99..8dd28bd75 100644 --- a/ui/tests/unit/views/Profiles.spec.ts +++ b/ui/tests/unit/views/Profiles.spec.ts @@ -103,6 +103,9 @@ describe("Profiles", () => { api.getProfiles.mockImplementationOnce(() => { return Promise.resolve(testData); }); + api.putProfile.mockImplementationOnce((profileJson: Profile) => { + return Promise.resolve(profileJson) + }); profileToAdd = { name: "profile2", From 5a088d8cc4c5451ff93952f336e3a388755774a3 Mon Sep 17 00:00:00 2001 From: SietsmaRJ Date: Thu, 12 Oct 2023 14:37:50 +0200 Subject: [PATCH 19/21] Processed PR feedback --- ui/tests/unit/views/Profiles.spec.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/ui/tests/unit/views/Profiles.spec.ts b/ui/tests/unit/views/Profiles.spec.ts index 8dd28bd75..407efce0e 100644 --- a/ui/tests/unit/views/Profiles.spec.ts +++ b/ui/tests/unit/views/Profiles.spec.ts @@ -79,8 +79,8 @@ describe("Profiles", () => { singleTestData = [ { - name: "profile1", - image: "source/some_profile1", + name: "profile-one", + image: "source/some_profile-one", host: "localhost", port: 6312, packageWhitelist: [ @@ -92,7 +92,7 @@ describe("Profiles", () => { "datashield.seed": "100000001" }, container: { - tags: ["source/some_profile1"], + tags: ["source/some_profile-two"], status: "NOT_RUNNING" } } @@ -108,8 +108,8 @@ describe("Profiles", () => { }); profileToAdd = { - name: "profile2", - image: "other_source/profile2", + name: "profile-two", + image: "other_source/profile-two", host: "localhost", port: 6313, packageWhitelist: [ @@ -121,7 +121,7 @@ describe("Profiles", () => { "datashield.seed": "100000002" }, container: { - tags: ["other_source/profile2"], + tags: ["other_source/profile-two"], status: "NOT_FOUND" } } @@ -176,7 +176,7 @@ describe("Profiles", () => { }); test("retrieve index of profile to edit", () => { - wrapper.vm.profileToEdit = "profile1"; + wrapper.vm.profileToEdit = "profile-one"; const index = wrapper.vm.getEditIndex(); expect(index).toBe(1); }); @@ -285,4 +285,14 @@ describe("Profiles", () => { await wrapper.vm.$nextTick(); expect(wrapper.vm.profiles[wrapper.vm.getEditIndex()].container.status).toBe("NOT_RUNNING"); }); + + test("creating a profile", async () => { + wrapper.vm.addNewProfile(); + wrapper.vm.profiles[0] = profileToAdd; + wrapper.vm.saveEditedProfile(); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.successMessage).toBe("[profile-two] was successfully saved."); + expect(wrapper.vm.errorMessage).toBe(""); + expect(wrapper.vm.profiles.includes(profileToAdd)).toBe(true); + }); }); From e3aee0153790a71328f7b6cbfe31b871829b0bc2 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 14 Nov 2023 11:11:06 +0100 Subject: [PATCH 20/21] test: get number of colunns programmatically --- ui/tests/unit/components/SimpleTable.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/unit/components/SimpleTable.spec.ts b/ui/tests/unit/components/SimpleTable.spec.ts index 3ff40b8fa..937aed65c 100644 --- a/ui/tests/unit/components/SimpleTable.spec.ts +++ b/ui/tests/unit/components/SimpleTable.spec.ts @@ -72,7 +72,7 @@ describe("SimpleTable", () => { props: { data: data, maxWidth: 600, - nCols: 7, + nCols: Object.keys(data[0]).length, nRows: data.length }, }); From 06c28c5426c2d135dabd2df943d8f376f5bcb0d3 Mon Sep 17 00:00:00 2001 From: mkslofstra Date: Tue, 14 Nov 2023 11:11:49 +0100 Subject: [PATCH 21/21] chore: remove unused import --- ui/tests/unit/views/ProjectExplorer.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/tests/unit/views/ProjectExplorer.spec.ts b/ui/tests/unit/views/ProjectExplorer.spec.ts index 62dde1e93..26c011fbd 100644 --- a/ui/tests/unit/views/ProjectExplorer.spec.ts +++ b/ui/tests/unit/views/ProjectExplorer.spec.ts @@ -2,7 +2,6 @@ import { shallowMount, VueWrapper } from "@vue/test-utils"; import ProjectsExplorer from "@/views/ProjectsExplorer.vue"; import { createRouter, createWebHistory } from "vue-router"; import * as _api from "@/api/api"; -import { ProjectsExplorerData, StringArray } from "@/types/types"; const api = _api as any;