diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ac323e4..93e03a7 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -22,4 +22,4 @@ jobs: - name: Build the solution run: dotnet build ./cli --no-restore --configuration Release - name: Run tests - run: dotnet test ./cli --no-build --configuration Release --verbosity normal \ No newline at end of file + run: VDK_TEST_MODE=1 dotnet test ./cli --no-build --configuration Release --verbosity normal \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1841901..1a349de 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ x86/ bld/ [Bb]in/ [Oo]bj/ + +# Allow code coverage output + [Ll]og/ [Ll]ogs/ [Pp]ackages @@ -487,3 +490,6 @@ $RECYCLE.BIN/ # Intellij *.iml + +!coverage.json +!coverage.xml \ No newline at end of file diff --git a/cli/.config/dotnet-tools.json b/cli/.config/dotnet-tools.json new file mode 100644 index 0000000..9af5f9d --- /dev/null +++ b/cli/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-coverage": { + "version": "17.14.2", + "commands": [ + "dotnet-coverage" + ] + } + } +} \ No newline at end of file diff --git a/cli/output.cobertura.xml b/cli/output.cobertura.xml new file mode 100644 index 0000000..5b417ff --- /dev/null +++ b/cli/output.cobertura.xml @@ -0,0 +1,8561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli/src/Vdk/Commands/CreateClusterCommand.cs b/cli/src/Vdk/Commands/CreateClusterCommand.cs index 5391d9e..2fba6a0 100644 --- a/cli/src/Vdk/Commands/CreateClusterCommand.cs +++ b/cli/src/Vdk/Commands/CreateClusterCommand.cs @@ -4,6 +4,9 @@ using Vdk.Data; using Vdk.Models; using Vdk.Services; +using k8s.Models; +using System.Diagnostics; +using KubeOps.KubernetesClient; using IConsole = Vdk.Services.IConsole; namespace Vdk.Commands; @@ -17,8 +20,9 @@ public class CreateClusterCommand : Command private readonly IKindClient _kind; private readonly IFluxClient _flux; private readonly IReverseProxyClient _reverseProxy; + private readonly Func _kubeClient; - public CreateClusterCommand(IConsole console, IEmbeddedDataReader dataReader, IYamlObjectSerializer yaml, IFileSystem fileSystem, IKindClient kind, IFluxClient flux, IReverseProxyClient reverseProxy) + public CreateClusterCommand(IConsole console, IEmbeddedDataReader dataReader, IYamlObjectSerializer yaml, IFileSystem fileSystem, IKindClient kind, IFluxClient flux, IReverseProxyClient reverseProxy, Func kubeClient) : base("cluster", "Create a Vega development cluster") { _console = console; @@ -28,6 +32,7 @@ public CreateClusterCommand(IConsole console, IEmbeddedDataReader dataReader, IY _kind = kind; _flux = flux; _reverseProxy = reverseProxy; + _kubeClient = kubeClient; var nameOption = new Option(new[] { "-n", "--Name" }, () => Defaults.ClusterName, "The name of the kind cluster to create."); var controlNodes = new Option(new[] { "-c", "--ControlPlaneNodes" }, () => Defaults.ControlPlaneNodes, "The number of control plane nodes in the cluster."); var workers = new Option(new[] { "-w", "--Workers" }, () => Defaults.WorkerNodes, "The number of worker nodes in the cluster."); @@ -36,11 +41,24 @@ public CreateClusterCommand(IConsole console, IEmbeddedDataReader dataReader, IY AddOption(controlNodes); AddOption(workers); AddOption(kubeVersion); - this.SetHandler(InvokeAsync, nameOption, controlNodes, workers, kubeVersion); + this.SetHandler((string name, int controlPlaneNodes, int workerNodes, string kubeVersion, bool bypassPrompt) => InvokeAsync(name, controlPlaneNodes, workerNodes, kubeVersion, bypassPrompt), + nameOption, controlNodes, workers, kubeVersion, new Option("--bypassPrompt", () => false, "Bypass the tenant prompt (for tests)")); } - public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPlaneNodes = 1, int workerNodes = 2, string kubeVersion = Defaults.KubeApiVersion) + public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPlaneNodes = 1, int workerNodes = 2, string kubeVersion = Defaults.KubeApiVersion, bool bypassPrompt = false) { + // Ensure config exists and prompt if not + var config = ConfigManager.EnsureConfig( + promptTenantId: () => { + _console.Write("Tenant GUID: "); + return Console.ReadLine(); + }, + openBrowser: () => { + var url = "https://archetypical.software/register"; + try { Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); } catch { } + }, + bypassPrompt: bypassPrompt); + var map = _dataReader.ReadJsonObjects("Vdk.Data.KindVersionData.json"); string? kindVersion = null; try @@ -136,6 +154,8 @@ public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPla } _flux.Bootstrap(name.ToLower(), "./clusters/default", branch: "main"); + + try { _reverseProxy.UpsertCluster(name.ToLower(), masterNode.ExtraPortMappings.First().HostPort, diff --git a/cli/src/Vdk/Commands/CreateRegistryCommand.cs b/cli/src/Vdk/Commands/CreateRegistryCommand.cs index 7bafd9d..de9f530 100644 --- a/cli/src/Vdk/Commands/CreateRegistryCommand.cs +++ b/cli/src/Vdk/Commands/CreateRegistryCommand.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using Vdk.Services; using IConsole = Vdk.Services.IConsole; @@ -18,7 +18,14 @@ public CreateRegistryCommand(IConsole console, IHubClient client): base("registr public Task InvokeAsync() { - _client.Create(); + try + { + _client.Create(); + } + catch (Exception ex) + { + _console.WriteError("Error creating registry: {0}", ex.Message); + } return Task.CompletedTask; } } \ No newline at end of file diff --git a/cli/src/Vdk/Commands/InitializeCommand.cs b/cli/src/Vdk/Commands/InitializeCommand.cs index 30d0c8e..46bf674 100644 --- a/cli/src/Vdk/Commands/InitializeCommand.cs +++ b/cli/src/Vdk/Commands/InitializeCommand.cs @@ -1,6 +1,7 @@ -using System.CommandLine; +using System.CommandLine; using Vdk.Constants; using Vdk.Services; +using System.Diagnostics; using IConsole = Vdk.Services.IConsole; namespace Vdk.Commands; @@ -21,11 +22,23 @@ public InitializeCommand(CreateClusterCommand createCluster, CreateProxyCommand _createRegistry = createRegistry; _kind = kind; _console = console; - this.SetHandler(InvokeAsync); + this.SetHandler((bool bypassPrompt) => InvokeAsync(bypassPrompt), new Option("--bypassPrompt", () => false, "Bypass the tenant prompt (for tests)")); } - public async Task InvokeAsync() + public async Task InvokeAsync(bool bypassPrompt = false) { + // Ensure config exists and prompt if not + var config = ConfigManager.EnsureConfig( + promptTenantId: () => { + _console.Write("Tenant GUID: "); + return Console.ReadLine(); + }, + openBrowser: () => { + var url = "https://archetypical.software/register"; + try { Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); } catch { } + }, + bypassPrompt: bypassPrompt); + var name = Defaults.ClusterName; var controlPlaneNodes = 1; var workerNodes = 2; diff --git a/cli/src/Vdk/Commands/RemoveProxyCommand.cs b/cli/src/Vdk/Commands/RemoveProxyCommand.cs index 5572514..14df03b 100644 --- a/cli/src/Vdk/Commands/RemoveProxyCommand.cs +++ b/cli/src/Vdk/Commands/RemoveProxyCommand.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using Vdk.Services; using IConsole = Vdk.Services.IConsole; @@ -18,7 +18,14 @@ public RemoveProxyCommand(IConsole console, IReverseProxyClient client) : base(" public Task InvokeAsync() { - _client.Delete(); + try + { + _client.Delete(); + } + catch (Exception ex) + { + _console.WriteError("Error removing proxy: {0}", ex.Message); + } return Task.CompletedTask; } } \ No newline at end of file diff --git a/cli/src/Vdk/Commands/RemoveRegistryCommand.cs b/cli/src/Vdk/Commands/RemoveRegistryCommand.cs index 3f98ecd..4ddcf63 100644 --- a/cli/src/Vdk/Commands/RemoveRegistryCommand.cs +++ b/cli/src/Vdk/Commands/RemoveRegistryCommand.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using Vdk.Services; using IConsole = Vdk.Services.IConsole; @@ -18,7 +18,14 @@ public RemoveRegistryCommand(IConsole console, IHubClient client) : base("regist public Task InvokeAsync() { - _client.Destroy(); + try + { + _client.Destroy(); + } + catch (Exception ex) + { + _console.WriteError("Error removing registry: {0}", ex.Message); + } return Task.CompletedTask; } } \ No newline at end of file diff --git a/cli/src/Vdk/Services/ConfigManager.cs b/cli/src/Vdk/Services/ConfigManager.cs new file mode 100644 index 0000000..a68574f --- /dev/null +++ b/cli/src/Vdk/Services/ConfigManager.cs @@ -0,0 +1,78 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +namespace Vdk.Services +{ + public class VdkConfig + { + public string TenantId { get; set; } = string.Empty; + public string Env { get; set; } = "VDK"; + } + + public static class ConfigManager + { + private static readonly string ConfigFileName = ".vdkconfig.json"; + private static string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigFileName); + + public static VdkConfig? LoadConfig() + { + if (!File.Exists(ConfigPath)) return null; + try + { + var json = File.ReadAllText(ConfigPath); + return JsonSerializer.Deserialize(json); + } + catch + { + return null; + } + } + + public static void SaveConfig(VdkConfig config) + { + var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(ConfigPath, json); + } + + public static VdkConfig EnsureConfig(Func promptTenantId, Action openBrowser, bool bypassPrompt = false) + { + if (bypassPrompt) + { + return new VdkConfig { TenantId = "test-tenant-id", Env = "Test" }; + } + // Bypass tenant prompt for unit tests + var testMode = Environment.GetEnvironmentVariable("VDK_TEST_MODE"); + if (!string.IsNullOrEmpty(testMode) && testMode.Equals("1")) + { + return new VdkConfig { TenantId = "test-tenant-id", Env = "Test" }; + } + var config = LoadConfig(); + if (config != null && !string.IsNullOrWhiteSpace(config.TenantId)) + return config; + + while (true) + { + // (Normal prompt logic continues here) + + Console.WriteLine("Enter your Tenant GUID (or leave blank to create an account):"); + var input = promptTenantId(); + if (string.IsNullOrWhiteSpace(input)) + { + Console.WriteLine("Opening browser to create an account..."); + openBrowser(); + continue; + } + // Validate GUID + if (Guid.TryParse(input, out _)) + { + config = new VdkConfig { TenantId = input.Trim() }; + SaveConfig(config); + return config; + } + Console.WriteLine("Invalid GUID. Please try again."); + } + } + } +} diff --git a/cli/src/Vdk/Services/KubernetesConfigMapHelper.cs b/cli/src/Vdk/Services/KubernetesConfigMapHelper.cs new file mode 100644 index 0000000..edb23fe --- /dev/null +++ b/cli/src/Vdk/Services/KubernetesConfigMapHelper.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using k8s.Models; + +namespace Vdk.Services +{ + public static class KubernetesConfigMapHelper + { + public static V1ConfigMap CreateClusterInfoConfigMap(string tenantId, string env) + { + return new V1ConfigMap + { + Metadata = new V1ObjectMeta + { + Name = "cluster-info", + NamespaceProperty = "vega-system" + }, + Data = new Dictionary + { + { "TenantId", tenantId }, + { "Env", env } + } + }; + } + } +} diff --git a/cli/src/Vdk/Services/ReverseProxyClient.cs b/cli/src/Vdk/Services/ReverseProxyClient.cs index 52c5186..bf1435a 100644 --- a/cli/src/Vdk/Services/ReverseProxyClient.cs +++ b/cli/src/Vdk/Services/ReverseProxyClient.cs @@ -1,5 +1,8 @@ +using System.Diagnostics; +using Docker.DotNet.Models; using k8s.Models; using KubeOps.KubernetesClient; +using System.Xml.Linq; using Vdk.Constants; using Vdk.Models; @@ -155,24 +158,24 @@ public void UpsertCluster(string clusterName, int targetPortHttps, int targetPor const int maxTimesWaiting = 60; while (!nsVegaExists && nTimesWaiting < maxTimesWaiting) { - if (_client(clusterName).Get("vega") == null) + if (_client(clusterName).Get("vega-system") == null) { nsVegaExists = false; if (nTimesWaiting % 5 == 0) - _console.WriteLine("Namespace 'vega' does not exist yet. Waiting..."); + _console.WriteLine("Namespace 'vega-system' does not exist yet. Waiting..."); Thread.Sleep(5000); nTimesWaiting++; } else { - _console.WriteLine("Namespace 'vega' already created by flux."); + _console.WriteLine("Namespace 'vega-system' already created by flux."); nsVegaExists = true; } } if (nTimesWaiting >= maxTimesWaiting) { - _console.WriteError("Namespace 'vega' does not exist after waiting. Please check the configuration and try again."); + _console.WriteError("Namespace 'vega-system' does not exist after waiting. Please check the configuration and try again."); return; } @@ -182,7 +185,7 @@ public void UpsertCluster(string clusterName, int targetPortHttps, int targetPor Metadata = new() { Name = "dev-tls", - NamespaceProperty = "vega" + NamespaceProperty = "vega-system" }, Type = "kubernetes.io/tls", Data = new Dictionary @@ -191,11 +194,41 @@ public void UpsertCluster(string clusterName, int targetPortHttps, int targetPor { "tls.key", File.ReadAllBytes("Certs/privkey.pem") } } }; - if (_client(clusterName).Get("dev-tls", "vega") == null) + if (_client(clusterName).Get("dev-tls", "vega-system") == null) { _console.WriteLine("Creating vega secret"); _client(clusterName).Create(tls); } + // Create ConfigMap after Flux bootstrap + var config = ConfigManager.EnsureConfig( + promptTenantId: () => { + _console.Write("Tenant GUID: "); + return Console.ReadLine(); + }, + openBrowser: () => { + var url = "https://archetypical.software/register"; + try { Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); } catch { } + }); + var configMap = KubernetesConfigMapHelper.CreateClusterInfoConfigMap(config.TenantId, config.Env); + try + { + var kubeClient = _client(clusterName); + var existing = kubeClient.Get("cluster-info", "vega-system"); + if (existing == null) + { + kubeClient.Create(configMap); + _console.WriteLine("Created ConfigMap 'cluster-info' in namespace 'vega-system'."); + } + else + { + kubeClient.Update(configMap); + _console.WriteLine("Updated ConfigMap 'cluster-info' in namespace 'vega-system'."); + } + } + catch (Exception ex) + { + _console.WriteError($"Failed to create/update ConfigMap: {ex.Message}"); + } ReloadConfigs(); } diff --git a/cli/tests/Vdk.Tests/CommandTestBase.cs b/cli/tests/Vdk.Tests/CommandTestBase.cs new file mode 100644 index 0000000..a414328 --- /dev/null +++ b/cli/tests/Vdk.Tests/CommandTestBase.cs @@ -0,0 +1,10 @@ +using Moq; +using Vdk.Services; + +namespace Vdk.Tests; + +public abstract class CommandTestBase +{ + protected Mock MockConsole { get; } = new Mock(); + // Add other common mocks or helpers here if needed +} diff --git a/cli/tests/Vdk.Tests/CreateClusterCommandTests.cs b/cli/tests/Vdk.Tests/CreateClusterCommandTests.cs new file mode 100644 index 0000000..ad8f892 --- /dev/null +++ b/cli/tests/Vdk.Tests/CreateClusterCommandTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.IO.Abstractions; +using KubeOps.KubernetesClient; +using Moq; +using Vdk.Commands; +using Vdk.Constants; +using Vdk.Data; +using Vdk.Models; +using Vdk.Services; +using Xunit; + +namespace Vdk.Tests; + +public class CreateClusterCommandTests : CommandTestBase +{ + private readonly Mock _mockDataReader = new(); + private readonly Mock _mockYaml = new(); + private readonly Mock _mockFileSystem = new(); + private readonly Mock _mockKind = new(); + private readonly Mock _mockFlux = new(); + private readonly Mock _mockReverseProxy = new(); + + private CreateClusterCommand CreateCommand() => + new CreateClusterCommand( + MockConsole.Object, + _mockDataReader.Object, + _mockYaml.Object, + _mockFileSystem.Object, + _mockKind.Object, + _mockFlux.Object, + _mockReverseProxy.Object, + s => Mock.Of() + ); + + [Fact] + public async Task InvokeAsync_ShouldHandleKindGetVersionThrows() + { + _mockKind.Setup(k => k.GetVersion()).Throws(new Exception("fail-version")); + var cmd = CreateCommand(); + await cmd.InvokeAsync(bypassPrompt: true); + MockConsole.Verify(c => c.WriteError(It.Is(msg => msg.Contains("Unable to retrieve the installed kind version."))), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleNullKindVersion() + { + _mockKind.Setup(k => k.GetVersion()).Returns((string)null); + _mockDataReader.Setup(d => d.ReadJsonObjects(It.IsAny())).Returns(new KindVersionMap()); + var cmd = CreateCommand(); + await cmd.InvokeAsync(bypassPrompt: true); + // The implementation writes a warning, not an error, for null/empty version + MockConsole.Verify(c => c.WriteWarning(It.IsAny()), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleCreateClusterThrows() + { + _mockKind.Setup(k => k.GetVersion()).Returns("v0.20.0"); + _mockKind.Setup(k => k.CreateCluster(It.IsAny(), It.IsAny())).Throws(new Exception("fail-create")); + _mockDataReader.Setup(d => d.ReadJsonObjects(It.IsAny())).Returns(new KindVersionMap { new KindVersion { Version = "v0.20.0", Images = new List { new KubeImage { Version = "1.29.0", Image = "img" } } } }); + _mockYaml.Setup(y => y.Serialize(It.IsAny())).Returns("manifest"); + _mockFileSystem.Setup(f => f.Path.GetTempFileName()).Returns("temp.yaml"); + var memStream = new System.IO.MemoryStream(); + var writer = new System.IO.StreamWriter(memStream); + _mockFileSystem.Setup(f => f.File.CreateText("temp.yaml")).Returns(writer); + var fileInfoMock = new Mock(); + fileInfoMock.Setup(f => f.FullName).Returns("ConfigMounts/hosts.toml"); + _mockFileSystem.Setup(f => f.FileInfo.New("ConfigMounts/hosts.toml")).Returns(fileInfoMock.Object); + var cmd = CreateCommand(); + await Assert.ThrowsAsync(() => cmd.InvokeAsync("test", 1, 2, "1.29", bypassPrompt: true)); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleMasterNodeNotFound() + { + _mockKind.Setup(k => k.GetVersion()).Returns("v0.20.0"); + _mockKind.Setup(k => k.CreateCluster(It.IsAny(), It.IsAny())).Verifiable(); + _mockDataReader.Setup(d => d.ReadJsonObjects(It.IsAny())).Returns(new KindVersionMap { new KindVersion { Version = "v0.20.0", Images = new List { new KubeImage { Version = "1.29.0", Image = "img" } } } }); + _mockYaml.Setup(y => y.Serialize(It.IsAny())).Returns("manifest"); + _mockFileSystem.Setup(f => f.Path.GetTempFileName()).Returns("temp.yaml"); + var memStream = new System.IO.MemoryStream(); + var writer = new System.IO.StreamWriter(memStream); + _mockFileSystem.Setup(f => f.File.CreateText("temp.yaml")).Returns(writer); + var fileInfoMock = new Mock(); + fileInfoMock.Setup(f => f.FullName).Returns("ConfigMounts/hosts.toml"); + _mockFileSystem.Setup(f => f.FileInfo.New("ConfigMounts/hosts.toml")).Returns(fileInfoMock.Object); + // Simulate scenario where no control-plane node with ExtraPortMappings exists (master node not found) + // This is handled internally by the command; no need to mock _kind.GetNodes + // Patch the test to simulate no nodes with ExtraPortMappings by passing 0 for both controlPlaneNodes and workerNodes + var cmd = CreateCommand(); + await cmd.InvokeAsync("test", 0, 0, "1.29", bypassPrompt: true); // No nodes created + MockConsole.Verify(c => c.WriteError(It.Is(msg => msg.Contains("Unable to find the master node"))), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleReverseProxyThrows() + { + _mockKind.Setup(k => k.GetVersion()).Returns("v0.20.0"); + _mockKind.Setup(k => k.CreateCluster(It.IsAny(), It.IsAny())).Verifiable(); + _mockDataReader.Setup(d => d.ReadJsonObjects(It.IsAny())).Returns(new KindVersionMap { new KindVersion { Version = "v0.20.0", Images = new List { new KubeImage { Version = "1.29.0", Image = "img" } } } }); + _mockYaml.Setup(y => y.Serialize(It.IsAny())).Returns("manifest"); + _mockFileSystem.Setup(f => f.Path.GetTempFileName()).Returns("temp.yaml"); + var memStream = new System.IO.MemoryStream(); + var writer = new System.IO.StreamWriter(memStream); + _mockFileSystem.Setup(f => f.File.CreateText("temp.yaml")).Returns(writer); + var fileInfoMock = new Mock(); + fileInfoMock.Setup(f => f.FullName).Returns("ConfigMounts/hosts.toml"); + _mockFileSystem.Setup(f => f.FileInfo.New("ConfigMounts/hosts.toml")).Returns(fileInfoMock.Object); + // The master node is created by the command logic; no need to mock _kind.GetNodes + // The command will create a control-plane node with ExtraPortMappings by default if controlPlaneNodes > 0 + _mockFlux.Setup(f => f.Bootstrap(It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); + _mockReverseProxy.Setup(r => r.UpsertCluster(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception("fail-proxy")); + var cmd = CreateCommand(); + await Assert.ThrowsAsync(() => cmd.InvokeAsync("test", 1, 2, "1.29", bypassPrompt: true)); + MockConsole.Verify(c => c.WriteLine(It.IsAny()), Times.AtLeastOnce); // stack trace + MockConsole.Verify(c => c.WriteError(It.Is(msg => msg.Contains("Failed to update reverse proxy"))), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldSucceed_NormalFlow() + { + _mockKind.Setup(k => k.GetVersion()).Returns("v0.20.0"); + _mockKind.Setup(k => k.CreateCluster(It.IsAny(), It.IsAny())).Verifiable(); + _mockDataReader.Setup(d => d.ReadJsonObjects(It.IsAny())).Returns(new KindVersionMap { new KindVersion { Version = "v0.20.0", Images = new List { new KubeImage { Version = "1.29.0", Image = "img" } } } }); + _mockYaml.Setup(y => y.Serialize(It.IsAny())).Returns("manifest"); + _mockFileSystem.Setup(f => f.Path.GetTempFileName()).Returns("temp.yaml"); + var memStream = new System.IO.MemoryStream(); + var writer = new System.IO.StreamWriter(memStream); + _mockFileSystem.Setup(f => f.File.CreateText("temp.yaml")).Returns(writer); + var fileInfoMock = new Mock(); + fileInfoMock.Setup(f => f.FullName).Returns("ConfigMounts/hosts.toml"); + _mockFileSystem.Setup(f => f.FileInfo.New("ConfigMounts/hosts.toml")).Returns(fileInfoMock.Object); + // The master node is created by the command logic; no need to mock _kind.GetNodes + // The command will create a control-plane node with ExtraPortMappings by default if controlPlaneNodes > 0 + _mockFlux.Setup(f => f.Bootstrap(It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); + _mockReverseProxy.Setup(r => r.UpsertCluster(It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); + var cmd = CreateCommand(); + await cmd.InvokeAsync("test", 1, 2, "1.29", bypassPrompt: true); + _mockKind.VerifyAll(); + _mockFlux.VerifyAll(); + _mockReverseProxy.VerifyAll(); + } +} diff --git a/cli/tests/Vdk.Tests/CreateRegistryCommandTests.cs b/cli/tests/Vdk.Tests/CreateRegistryCommandTests.cs new file mode 100644 index 0000000..4256602 --- /dev/null +++ b/cli/tests/Vdk.Tests/CreateRegistryCommandTests.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Moq; +using Vdk.Commands; +using Vdk.Services; +using Xunit; + +namespace Vdk.Tests; + +public class CreateRegistryCommandTests : CommandTestBase +{ + [Fact] + public async Task InvokeAsync_ShouldCallCreate() + { + var mockHubClient = new Mock(); + var cmd = new CreateRegistryCommand(MockConsole.Object, mockHubClient.Object); + await cmd.InvokeAsync(); + mockHubClient.Verify(h => h.Create(), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleExceptionAndWriteError() + { + var mockHubClient = new Mock(); + mockHubClient.Setup(h => h.Create()).Throws(new System.Exception("fail!")); + var cmd = new CreateRegistryCommand(MockConsole.Object, mockHubClient.Object); + await cmd.InvokeAsync(); + MockConsole.Verify(c => c.WriteError(It.Is(msg => msg.Contains("Error creating registry")), "fail!"), Times.Once); + } + + [Fact] + public async Task Handler_ShouldBeRegistered() + { + var mockHubClient = new Mock(); + var cmd = new CreateRegistryCommand(MockConsole.Object, mockHubClient.Object); + var invoked = false; + mockHubClient.Setup(h => h.Create()).Callback(() => invoked = true); + await cmd.InvokeAsync(); + Assert.True(invoked); + } +} + diff --git a/cli/tests/Vdk.Tests/KindClientTests.cs b/cli/tests/Vdk.Tests/KindClientTests.cs new file mode 100644 index 0000000..5642bf2 --- /dev/null +++ b/cli/tests/Vdk.Tests/KindClientTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using Moq; +using Vdk.Services; +using Xunit; + +namespace Vdk.Tests; + +public class KindClientTests +{ + + [Fact] + public void CreateCluster_ShouldCallShellWithCorrectArgs() + { + var mockConsole = new Mock(); + var mockShell = new Mock(); + var client = new KindClient(mockConsole.Object, mockShell.Object); + mockShell.Setup(s => s.Execute("kind", It.Is(args => args[0] == "create" && args[1] == "cluster"))).Verifiable(); + client.CreateCluster("test", "config.yaml"); + mockShell.VerifyAll(); + } + + [Fact] + public void DeleteCluster_ShouldCallShellWithCorrectArgs() + { + var mockConsole = new Mock(); + var mockShell = new Mock(); + var client = new KindClient(mockConsole.Object, mockShell.Object); + mockShell.Setup(s => s.Execute("kind", It.Is(args => args[0] == "delete" && args[1] == "cluster"))).Verifiable(); + client.DeleteCluster("test"); + mockShell.VerifyAll(); + } + + [Fact] + public void ListClusters_ShouldReturnParsedList() + { + var mockConsole = new Mock(); + var mockShell = new Mock(); + var client = new KindClient(mockConsole.Object, mockShell.Object); + mockShell.Setup(s => s.ExecuteAndCapture("kind", It.Is(a => a != null && a.SequenceEqual(new[] { "get", "clusters" })))) + .Callback((string command, string[] args) => + { + System.Diagnostics.Debug.WriteLine($"MOCK HIT: command={command}, args=[{string.Join(",", args)}]"); + }) + .Returns($"test1{Environment.NewLine}test2{Environment.NewLine}"); + var clusters = client.ListClusters(); + Assert.Equal(new List { "test1", "test2" }, clusters); + } + + [Fact] + public void ListClusters_ShouldReturnEmptyListOnEmptyOutput() + { + var mockConsole = new Mock(); + var mockShell = new Mock(); + var client = new KindClient(mockConsole.Object, mockShell.Object); + mockShell.Setup(s => s.ExecuteAndCapture("kind", It.Is(a => a != null && a.SequenceEqual(new[] { "get", "clusters" })))) + .Callback((string command, string[] args) => + { + System.Diagnostics.Debug.WriteLine($"MOCK HIT: command={command}, args=[{string.Join(",", args)}]"); + }) + .Returns(""); + var clusters = client.ListClusters(); + Assert.Empty(clusters); + } + + [Fact] + public void GetVersion_ShouldReturnParsedVersion() + { + var mockConsole = new Mock(); + var mockShell = new Mock(); + var client = new KindClient(mockConsole.Object, mockShell.Object); + mockShell.Setup(s => s.ExecuteAndCapture("kind", It.Is(a => a != null && a.SequenceEqual(new[] { "--version" })))) + .Callback((string command, string[] args) => + { + System.Diagnostics.Debug.WriteLine($"MOCK HIT: command={command}, args=[{string.Join(",", args)}]"); + }) + .Returns("kind version 0.27.0"); + var version = client.GetVersion(); + System.Diagnostics.Debug.WriteLine($"Actual version: {version}"); + Assert.Equal("0.27.0", version); + } + + [Fact] + public void GetVersion_ShouldReturnNullOnShortString() + { + var mockConsole = new Mock(); + var mockShell = new Mock(); + var client = new KindClient(mockConsole.Object, mockShell.Object); + mockShell.Setup(s => s.ExecuteAndCapture("kind", It.Is(a => a != null && a.SequenceEqual(new[] { "--version" })))) + .Callback((string command, string[] args) => + { + System.Diagnostics.Debug.WriteLine($"MOCK HIT: command={command}, args=[{string.Join(",", args)}]"); + }) + .Returns("short"); + var version = client.GetVersion(); + System.Diagnostics.Debug.WriteLine($"Actual version (short): {version}"); + Assert.Null(version); + } +} diff --git a/cli/tests/Vdk.Tests/ListClustersCommandTests.cs b/cli/tests/Vdk.Tests/ListClustersCommandTests.cs new file mode 100644 index 0000000..b116563 --- /dev/null +++ b/cli/tests/Vdk.Tests/ListClustersCommandTests.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Moq; +using Vdk.Commands; +using Vdk.Services; +using Xunit; + +namespace Vdk.Tests; + +public class ListClustersCommandTests : CommandTestBase +{ + [Fact] + public async Task InvokeAsync_ShouldCallListClustersAndWriteLines() + { + var mockKindClient = new Mock(); + mockKindClient.Setup(k => k.ListClusters()).Returns(new System.Collections.Generic.List { "test-cluster" }); + + var cmd = new ListClustersCommand(MockConsole.Object, mockKindClient.Object); + await cmd.InvokeAsync(); + + mockKindClient.Verify(k => k.ListClusters(), Times.Once); + MockConsole.Verify(c => c.WriteLine("test-cluster"), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleEmptyList() + { + var mockKindClient = new Mock(); + mockKindClient.Setup(k => k.ListClusters()).Returns(new System.Collections.Generic.List()); + var cmd = new ListClustersCommand(MockConsole.Object, mockKindClient.Object); + await cmd.InvokeAsync(); + // Should not throw, and WriteLine should not be called + MockConsole.Verify(c => c.WriteLine(It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handler_ShouldBeRegistered() + { + var mockKindClient = new Mock(); + var cmd = new ListClustersCommand(MockConsole.Object, mockKindClient.Object); + var invoked = false; + mockKindClient.Setup(k => k.ListClusters()).Callback(() => invoked = true).Returns(new System.Collections.Generic.List()); + await cmd.InvokeAsync(); + Assert.True(invoked); + } +} + diff --git a/cli/tests/Vdk.Tests/RemoveClusterCommandTests.cs b/cli/tests/Vdk.Tests/RemoveClusterCommandTests.cs new file mode 100644 index 0000000..6f9330c --- /dev/null +++ b/cli/tests/Vdk.Tests/RemoveClusterCommandTests.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Moq; +using Vdk.Commands; +using Vdk.Services; +using Xunit; + +namespace Vdk.Tests; + +public class RemoveClusterCommandTests : CommandTestBase +{ + [Fact] + public async Task InvokeAsync_ShouldCallDeleteCluster() + { + var mockKindClient = new Mock(); + var cmd = new RemoveClusterCommand(MockConsole.Object, mockKindClient.Object); + await cmd.InvokeAsync("test-cluster"); + mockKindClient.Verify(k => k.DeleteCluster("test-cluster"), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldWriteError_WhenDeleteClusterThrows() + { + var mockKindClient = new Mock(); + mockKindClient.Setup(k => k.DeleteCluster(It.IsAny())).Throws(new System.Exception("fail!")); + var cmd = new RemoveClusterCommand(MockConsole.Object, mockKindClient.Object); + await cmd.InvokeAsync("test-cluster"); + MockConsole.Verify(c => c.WriteError(It.Is(msg => msg.Contains("Error removing cluster '{ClusterName}': {ErrorMessage}")), "test-cluster", "fail!"), Times.Once); + } + + [Fact] + public async Task Handler_ShouldBeRegistered() + { + var mockKindClient = new Mock(); + var cmd = new RemoveClusterCommand(MockConsole.Object, mockKindClient.Object); + var invoked = false; + mockKindClient.Setup(k => k.DeleteCluster(It.IsAny())).Callback(() => invoked = true); + await cmd.InvokeAsync("test-cluster"); + Assert.True(invoked); + } +} + diff --git a/cli/tests/Vdk.Tests/RemoveProxyCommandTests.cs b/cli/tests/Vdk.Tests/RemoveProxyCommandTests.cs new file mode 100644 index 0000000..a208171 --- /dev/null +++ b/cli/tests/Vdk.Tests/RemoveProxyCommandTests.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Moq; +using Vdk.Commands; +using Vdk.Services; +using Xunit; + +namespace Vdk.Tests; + +public class RemoveProxyCommandTests : CommandTestBase +{ + [Fact] + public async Task InvokeAsync_ShouldCallDelete() + { + var mockProxyClient = new Mock(); + var cmd = new RemoveProxyCommand(MockConsole.Object, mockProxyClient.Object); + await cmd.InvokeAsync(); + mockProxyClient.Verify(p => p.Delete(), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleExceptionAndWriteError() + { + var mockProxyClient = new Mock(); + mockProxyClient.Setup(p => p.Delete()).Throws(new System.Exception("fail!")); + var cmd = new RemoveProxyCommand(MockConsole.Object, mockProxyClient.Object); + await cmd.InvokeAsync(); + MockConsole.Verify(c => c.WriteError(It.Is(msg => msg.Contains("Error removing proxy")), "fail!"), Times.Once); + } + + [Fact] + public async Task Handler_ShouldBeRegistered() + { + var mockProxyClient = new Mock(); + var cmd = new RemoveProxyCommand(MockConsole.Object, mockProxyClient.Object); + var invoked = false; + mockProxyClient.Setup(p => p.Delete()).Callback(() => invoked = true); + await cmd.InvokeAsync(); + Assert.True(invoked); + } +} + diff --git a/cli/tests/Vdk.Tests/RemoveRegistryCommandTests.cs b/cli/tests/Vdk.Tests/RemoveRegistryCommandTests.cs new file mode 100644 index 0000000..0911699 --- /dev/null +++ b/cli/tests/Vdk.Tests/RemoveRegistryCommandTests.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Moq; +using Vdk.Commands; +using Vdk.Services; +using Xunit; + +namespace Vdk.Tests; + +public class RemoveRegistryCommandTests : CommandTestBase +{ + [Fact] + public async Task InvokeAsync_ShouldCallDestroy() + { + var mockHubClient = new Mock(); + var cmd = new RemoveRegistryCommand(MockConsole.Object, mockHubClient.Object); + await cmd.InvokeAsync(); + mockHubClient.Verify(h => h.Destroy(), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldHandleExceptionAndWriteError() + { + var mockHubClient = new Mock(); + mockHubClient.Setup(h => h.Destroy()).Throws(new System.Exception("fail!")); + var cmd = new RemoveRegistryCommand(MockConsole.Object, mockHubClient.Object); + await cmd.InvokeAsync(); + MockConsole.Verify(c => c.WriteError(It.Is(msg => msg.Contains("Error removing registry")), "fail!"), Times.Once); + } + + [Fact] + public async Task Handler_ShouldBeRegistered() + { + var mockHubClient = new Mock(); + var cmd = new RemoveRegistryCommand(MockConsole.Object, mockHubClient.Object); + var invoked = false; + mockHubClient.Setup(h => h.Destroy()).Callback(() => invoked = true); + await cmd.InvokeAsync(); + Assert.True(invoked); + } +} + diff --git a/cli/tests/Vdk.Tests/TestEnvironmentFixture.cs b/cli/tests/Vdk.Tests/TestEnvironmentFixture.cs new file mode 100644 index 0000000..df82db4 --- /dev/null +++ b/cli/tests/Vdk.Tests/TestEnvironmentFixture.cs @@ -0,0 +1,12 @@ +using System; +using Xunit; + +public class TestEnvironmentFixture +{ + public TestEnvironmentFixture() + { + // Set the environment variable before any tests run + Environment.SetEnvironmentVariable("VDK_TEST_MODE", "1"); + } +} + diff --git a/cli/tests/Vdk.Tests/TestSetup.cs b/cli/tests/Vdk.Tests/TestSetup.cs new file mode 100644 index 0000000..355206c --- /dev/null +++ b/cli/tests/Vdk.Tests/TestSetup.cs @@ -0,0 +1,16 @@ +using System; +using Xunit; + +// This class will be constructed by xUnit before any tests run +public class TestSetup : IDisposable +{ + static TestSetup() + { + // Ensure the test environment variable is set for all tests + Environment.SetEnvironmentVariable("VDK_TEST_MODE", "1"); + } + + public TestSetup() { } + public void Dispose() { } +} + diff --git a/cli/tests/Vdk.Tests/Vdk.Tests.csproj b/cli/tests/Vdk.Tests/Vdk.Tests.csproj index 370275a..791f4af 100644 --- a/cli/tests/Vdk.Tests/Vdk.Tests.csproj +++ b/cli/tests/Vdk.Tests/Vdk.Tests.csproj @@ -12,7 +12,7 @@ - + @@ -20,10 +20,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/cli/tests/Vdk.Tests/vega_coverage.json b/cli/tests/Vdk.Tests/vega_coverage.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/cli/tests/Vdk.Tests/vega_coverage.json @@ -0,0 +1 @@ +{} \ No newline at end of file