diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 48345732..c915819a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,6 +53,7 @@ jobs: runs-on: ubuntu-22.04 env: BATS_LIB_PATH: /usr/lib + TEST_TERMINAL_WIDTH: 200 steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Check out platform @@ -110,3 +111,4 @@ jobs: - run: tests/kas-grants.bats - run: tests/profile.bats - run: tests/kas-registry.bats + - run: tests/namespaces.bats diff --git a/Makefile b/Makefile index 4e3cbd6f..0d0252db 100644 --- a/Makefile +++ b/Makefile @@ -87,7 +87,7 @@ build-test: .PHONY: test-bats test-bats: build-test - bats ./tests + ./tests/resize_terminal.sh && bats ./tests # Target for cleaning up the target directory .PHONY: clean diff --git a/README.md b/README.md index aa80a4d7..d1953739 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,14 @@ prerequisites are met: - See the [platform README](https://github.com/opentdf/platform) for instructions To run the tests you can either run `make test-bats` or execute specific test suites with `bats tests/.bats`. + +#### Terminal Size + +Some tests for output rendered in the terminal will vary in behavior depending on terminal size. + +Terminal size when testing: + +1. set to standard defaults if running `make test-bats` +2. can be set manually by mouse in terminal where tests are triggered +3. can be set by argument `./tests/resize_terminal.sh < rows height > < columns width >` +4. can be set by environment variable, i.e. `export TEST_TERMINAL_WIDTH="200"` (200 is columns width) diff --git a/cmd/policy-attributeNamespaces.go b/cmd/policy-attributeNamespaces.go index 8e8b5b89..623cf6a3 100644 --- a/cmd/policy-attributeNamespaces.go +++ b/cmd/policy-attributeNamespaces.go @@ -104,6 +104,7 @@ func policy_deactivateAttributeNamespace(cmd *cobra.Command, args []string) { h := NewHandler(c) defer h.Close() + force := c.Flags.GetOptionalBool("force") id := c.Flags.GetRequiredString("id") ns, err := h.GetNamespace(id) @@ -112,7 +113,9 @@ func policy_deactivateAttributeNamespace(cmd *cobra.Command, args []string) { cli.ExitWithError(errMsg, err) } - cli.ConfirmAction(cli.ActionDeactivate, "namespace", ns.Name, false) + if !force { + cli.ConfirmAction(cli.ActionDeactivate, "namespace", ns.Name, false) + } d, err := h.DeactivateNamespace(id) if err != nil { @@ -312,6 +315,11 @@ func init() { deactivateCmd.GetDocFlag("id").Default, deactivateCmd.GetDocFlag("id").Description, ) + deactivateCmd.Flags().Bool( + deactivateCmd.GetDocFlag("force").Name, + false, + deactivateCmd.GetDocFlag("force").Description, + ) // unsafe unsafeCmd := man.Docs.GetCommand("policy/attributes/namespaces/unsafe") diff --git a/docs/man/policy/attributes/namespaces/deactivate.md b/docs/man/policy/attributes/namespaces/deactivate.md index 07954252..decdd0b1 100644 --- a/docs/man/policy/attributes/namespaces/deactivate.md +++ b/docs/man/policy/attributes/namespaces/deactivate.md @@ -7,6 +7,8 @@ command: shorthand: i description: ID of the attribute namespace required: true + - name: force + description: Force deletion without interactive confirmation (dangerous) --- Deactivating an Attribute Namespace will make the namespace name inactive as well as any attribute definitions and values beneath. @@ -16,4 +18,4 @@ Deactivation of a Namespace renders any existing TDFs of those attributes inacce Deactivation will permanently reserve the Namespace name within a platform. Reactivation and deletion are both considered "unsafe" behaviors. -For reactivation, see the `unsafe` command. \ No newline at end of file +For reactivation, see the `unsafe` command. diff --git a/pkg/cli/flagValues.go b/pkg/cli/flagValues.go index 4938d331..76944b74 100644 --- a/pkg/cli/flagValues.go +++ b/pkg/cli/flagValues.go @@ -25,7 +25,7 @@ func newFlagHelper(cmd *cobra.Command) *flagHelper { func (f flagHelper) GetRequiredString(flag string) string { v := f.cmd.Flag(flag).Value.String() if v == "" { - fmt.Println(ErrorMessage("Flag "+flag+" is required", nil)) + fmt.Println(ErrorMessage("Flag '--"+flag+"' is required", nil)) os.Exit(1) } return v @@ -41,11 +41,11 @@ func (f flagHelper) GetOptionalString(flag string) string { func (f flagHelper) GetStringSlice(flag string, v []string, opts FlagsStringSliceOptions) []string { if len(v) < opts.Min { - fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at least %d non-empty values", flag, opts.Min), nil)) + fmt.Println(ErrorMessage(fmt.Sprintf("Flag '--%s' must have at least %d non-empty values", flag, opts.Min), nil)) os.Exit(1) } if opts.Max > 0 && len(v) > opts.Max { - fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at most %d non-empty values", flag, opts.Max), nil)) + fmt.Println(ErrorMessage(fmt.Sprintf("Flag '--%s' must have at most %d non-empty values", flag, opts.Max), nil)) os.Exit(1) } return v @@ -54,7 +54,7 @@ func (f flagHelper) GetStringSlice(flag string, v []string, opts FlagsStringSlic func (f flagHelper) GetRequiredInt32(flag string) int32 { v, e := f.cmd.Flags().GetInt32(flag) if e != nil { - fmt.Println(ErrorMessage("Flag "+flag+" is required", nil)) + fmt.Println(ErrorMessage("Flag '--"+flag+"' is required", nil)) os.Exit(1) } // if v == 0 { @@ -72,7 +72,7 @@ func (f flagHelper) GetOptionalBool(flag string) bool { func (f flagHelper) GetRequiredBool(flag string) bool { v, e := f.cmd.Flags().GetBool(flag) if e != nil { - fmt.Println(ErrorMessage("Flag "+flag+" is required", nil)) + fmt.Println(ErrorMessage("Flag '--"+flag+"' is required", nil)) os.Exit(1) } return v diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go index 248cc784..7e4d4a9d 100644 --- a/pkg/cli/utils.go +++ b/pkg/cli/utils.go @@ -1,8 +1,11 @@ package cli import ( + "os" + "strconv" "strings" + "github.com/opentdf/otdfctl/pkg/config" "golang.org/x/term" ) @@ -10,9 +13,21 @@ func CommaSeparated(values []string) string { return "[" + strings.Join(values, ", ") + "]" } +// Returns the terminal width (overridden by the TEST_TERMINAL_WIDTH env var for testing) func TermWidth() int { - w, _, err := term.GetSize(0) - if err != nil { + var ( + w int + err error + ) + testSize := os.Getenv(config.TEST_TERMINAL_WIDTH) + if testSize == "" { + w, _, err = term.GetSize(0) + if err != nil { + return 80 + } + return w + } + if w, err = strconv.Atoi(testSize); err != nil { return 80 } return w diff --git a/pkg/config/config.go b/pkg/config/config.go index 46dcf66c..4e2d9e50 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -24,6 +24,9 @@ var ( // Test mode is used to determine if the application is running in test mode // "true" = running in test mode TestMode = "" + + // Test terminal size is a runtime env var to allow for testing of terminal output + TEST_TERMINAL_WIDTH = "TEST_TERMINAL_WIDTH" ) type Output struct { diff --git a/tests/namespace.bats b/tests/namespace.bats deleted file mode 100755 index 45f0de71..00000000 --- a/tests/namespace.bats +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bats - -# Tests for namespaces - -# Create namespace - -# Get namesapce - -# Update namespace - -# List namespaces - -# Deactivate namespace - -# Unsafe namespace - -# Unsafe namespace - -# Cleanup - delete everything \ No newline at end of file diff --git a/tests/namespaces.bats b/tests/namespaces.bats new file mode 100755 index 00000000..403940a5 --- /dev/null +++ b/tests/namespaces.bats @@ -0,0 +1,172 @@ +#!/usr/bin/env bats + +# Tests for namespaces + +setup_file() { + echo -n '{"clientId":"opentdf","clientSecret":"secret"}' > creds.json + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # Create the namespace to be used by other tests + + export NS_NAME="creating-test-ns.net" + export NS_NAME_UPDATE="updated-test-ns.net" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create -n "$NS_NAME" --json | jq -r '.id') + export NS_ID_FLAG="--id $NS_ID" +} + +setup() { + load "${BATS_LIB_PATH}/bats-support/load.bash" + load "${BATS_LIB_PATH}/bats-assert/load.bash" + + # invoke binary with credentials + run_otdfctl_ns () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy attributes namespaces $*" + } +} + +teardown_file() { + # clear out all test env vars + unset HOST WITH_CREDS NS_NAME NS_FQN NS_ID NS_ID_FLAG +} + +@test "Create a namespace - Good" { + run_otdfctl_ns create --name throwaway.test + assert_output --partial "SUCCESS" + assert_output --regexp "Name.*throwaway.test" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_output --regexp "Updated At" +} + +@test "Create a namespace - Bad" { + # bad namespace names + run_otdfctl_ns create --name no_domain_extension + assert_failure + run_otdfctl_ns create --name -first-char-hyphen.co + assert_failure + run_otdfctl_ns create --name last-char-hyphen-.co + assert_failure + + # missing flag + run_otdfctl_ns create + assert_failure + assert_output --partial "Flag '--name' is required" + + # conflict + run_otdfctl_ns create -n "$NS_NAME" + assert_failure + assert_output --partial "AlreadyExists" +} + +@test "Get a namespace - Good" { + run_otdfctl_ns get "$NS_ID_FLAG" + assert_success + assert_output --regexp "Id.*$NS_ID" + assert_output --regexp "Name.*$NS_NAME" + + echo $NS_ID + run_otdfctl_ns get "$NS_ID_FLAG" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$NS_ID" ] + [ "$(echo "$output" | jq -r '.name')" = "$NS_NAME" ] +} + +@test "List namespaces - when active" { + run_otdfctl_ns list --json + echo $output | jq --arg id "$NS_ID" '.[] | select(.[]? | type == "object" and .id == $id)' + + run_otdfctl_ns list --state inactive --json + echo $output | refute_output --partial "$NS_ID" + + run_otdfctl_ns list --state active + echo $output | assert_output --partial "$NS_ID" +} + +@test "Update namespace - Safe" { + # extend labels + run_otdfctl_ns update "$NS_ID_FLAG" -l key=value --label test=true + assert_success + assert_output --regexp "Id.*$NS_ID" + assert_output --regexp "Name.*$NS_NAME" + assert_output --regexp "Labels.*key: value" + assert_output --regexp "Labels.*test: true" + + # force replace labels + run_otdfctl_ns update "$NS_ID_FLAG" -l key=other --force-replace-labels + assert_success + assert_output --regexp "Id.*$NS_ID" + assert_output --regexp "Name.*$NS_NAME" + assert_output --regexp "Labels.*key: other" + refute_output --regexp "Labels.*key: value" + refute_output --regexp "Labels.*test: true" +} + +@test "Update namespace - Unsafe" { + run_otdfctl_ns unsafe update "$NS_ID_FLAG" -n "$NS_NAME_UPDATE" --force + assert_success + assert_output --regexp "Id.*$NS_ID" + run_otdfctl_ns get "$NS_ID_FLAG" + assert_output --regexp "Name.*$NS_NAME_UPDATE" + refute_output --regexp "Name.*$NS_NAME" +} + +@test "Deactivate namespace" { + run_otdfctl_ns deactivate "$NS_ID_FLAG" --force + assert_success + assert_output --regexp "Id.*$NS_ID" + assert_output --regexp "Id.*$NS_NAME_UPDATE" +} + +@test "List namespaces - when inactive" { + run_otdfctl_ns list --json + echo $output | jq --arg id "$NS_ID" '.[] | select(.[]? | type == "object" and .id == $id)' + + # json + run_otdfctl_ns list --state inactive --json + echo $output | assert_output --partial "$NS_ID" + + run_otdfctl_ns list --state active --json + echo $output | refute_output --partial "$NS_ID" + # table + run_otdfctl_ns list --state inactive + echo $output | assert_output --partial "$NS_ID" + + run_otdfctl_ns list --state active + echo $output | refute_output --partial "$NS_ID" +} + +@test "Unsafe reactivate namespace" { + run_otdfctl_ns unsafe reactivate "$NS_ID_FLAG" --force + assert_success + assert_output --regexp "Id.*$NS_ID" +} + +@test "List namespaces - when reactivated" { + run_otdfctl_ns list --json + echo $output | jq --arg id "$NS_ID" '.[] | select(.[]? | type == "object" and .id == $id)' + + run_otdfctl_ns list --state inactive --json + echo $output | refute_output --partial "$NS_ID" + + run_otdfctl_ns list --state active + echo $output | assert_output --partial "$NS_ID" +} + +@test "Unsafe delete namespace" { + run_otdfctl_ns unsafe delete "$NS_ID_FLAG" --force + assert_success + assert_output --regexp "Id.*$NS_ID" + assert_output --regexp "Id.*$NS_NAME_UPDATE" +} + +@test "List namespaces - when deleted" { + run_otdfctl_ns list --json + echo $output | refute_output --partial "$NS_ID" + + run_otdfctl_ns list --state inactive --json + echo $output | refute_output --partial "$NS_ID" + + run_otdfctl_ns list --state active + echo $output | refute_output --partial "$NS_ID" +} diff --git a/tests/profile.bats b/tests/profile.bats index 58d7204c..f0ed9cb8 100755 --- a/tests/profile.bats +++ b/tests/profile.bats @@ -1,27 +1,8 @@ #!/usr/bin/env bats -setup() { - bats_require_minimum_version 1.5.0 - +setup() { OTDFCTL_BIN=./otdfctl_testbuild - if [[ $(which bats) == *"homebrew"* ]]; then - BATS_LIB_PATH=$(brew --prefix)/lib - fi - - # Check if BATS_LIB_PATH environment variable exists - if [ -z "${BATS_LIB_PATH}" ]; then - # Check if bats bin has homebrew in path name - if [[ $(which bats) == *"homebrew"* ]]; then - BATS_LIB_PATH=$(dirname $(which bats))/../lib - elif [ -d "/usr/lib/bats-support" ]; then - BATS_LIB_PATH="/usr/lib" - elif [ -d "/usr/local/lib/bats-support" ]; then - # Check if bats-support exists in /usr/local/lib - BATS_LIB_PATH="/usr/local/lib" - fi - fi - echo "BATS_LIB_PATH: $BATS_LIB_PATH" load "${BATS_LIB_PATH}/bats-support/load.bash" load "${BATS_LIB_PATH}/bats-assert/load.bash" diff --git a/tests/resize_terminal.sh b/tests/resize_terminal.sh new file mode 100755 index 00000000..a2a65680 --- /dev/null +++ b/tests/resize_terminal.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +#### +# Make sure we have a terminal size large enough to test table output +#### + +## Accepts two arguments: rows and columns (both integers) + +# Default terminal size +DEFAULT_ROWS=40 +DEFAULT_COLUMNS=200 + +# Set rows and columns to the defaults or use the provided arguments +ROWS=${1:-$DEFAULT_ROWS} +COLUMNS=${2:-$DEFAULT_COLUMNS} + +set_terminal_size_linux() { + if command -v resize &> /dev/null; then + resize -s "$ROWS" "$COLUMNS" + else + export COLUMNS="$COLUMNS" + export LINES="$ROWS" + fi +} + +set_terminal_size_mac() { + printf '\e[8;%d;%dt' "$ROWS" "$COLUMNS" +} + +set_terminal_size_windows() { + if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + printf '\e[8;%d;%dt' "$ROWS" "$COLUMNS" + else + cmd.exe /c "mode con: cols=$COLUMNS lines=$ROWS" + fi +} + +# Detect the OS and set the terminal size appropriately +case "$OSTYPE" in + linux*) + set_terminal_size_linux + ;; + darwin*) + set_terminal_size_mac + ;; + msys* | cygwin* | win*) + set_terminal_size_windows + ;; + *) + echo "Unsupported OS: $OSTYPE" + ;; +esac \ No newline at end of file diff --git a/tests/setup_suite.bash b/tests/setup_suite.bash new file mode 100755 index 00000000..871d804d --- /dev/null +++ b/tests/setup_suite.bash @@ -0,0 +1,29 @@ +#!/bin/bash + +#### +# Make sure we can load BATS dependencies +#### + +setup_suite(){ + + bats_require_minimum_version 1.7.0 + + if [[ $(which bats) == *"homebrew"* ]]; then + BATS_LIB_PATH=$(brew --prefix)/lib + fi + + # Check if BATS_LIB_PATH environment variable exists + if [ -z "${BATS_LIB_PATH}" ]; then + # Check if bats bin has homebrew in path name + if [[ $(which bats) == *"homebrew"* ]]; then + BATS_LIB_PATH=$(dirname $(which bats))/../lib + elif [ -d "/usr/lib/bats-support" ]; then + BATS_LIB_PATH="/usr/lib" + elif [ -d "/usr/local/lib/bats-support" ]; then + # Check if bats-support exists in /usr/local/lib + BATS_LIB_PATH="/usr/local/lib" + fi + fi + echo "BATS_LIB_PATH: $BATS_LIB_PATH" + export BATS_LIB_PATH=$BATS_LIB_PATH +} \ No newline at end of file