diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 88a1dfbc..3bff8878 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -98,9 +98,12 @@ jobs: - name: build the CLI run: go build . + - name: build the CLI in test mode + run: make build-test - name: set up the config run: cp otdfctl-example.yaml otdfctl.yaml - name: Setup Bats and bats libs uses: bats-core/bats-action@2.0.0 - run: tests/encrypt-decrypt.bats - run: tests/kas-grants.bats + - run: tests/profile.bats diff --git a/.gitignore b/.gitignore index c3c02e6c..a28c1f61 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,14 @@ target/ .vscode/launch.json otdfctl.yaml -# Ignore the tructl binary +# Ignore the binaries otdfctl +otdfctl.* +otdfctl_testbuild +otdfctl_testbuild.* + +# Test artifacts +creds.json # Hugo public/ diff --git a/Makefile b/Makefile index 2c864e86..4e3cbd6f 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,11 @@ GO_MOD_LINE = $(shell head -n 1 go.mod | cut -c 8-) GO_MOD_NAME = $(word 1,$(subst /, ,$(GO_MOD_LINE))) APP_CFG = $(GO_MOD_LINE)/pkg/config -GO_BUILD_FLAGS=-ldflags "-X $(APP_CFG).Version=${CURR_VERSION} -X $(APP_CFG).CommitSha=${COMMIT_SHA} -X $(APP_CFG).BuildTime=${BUILD_TIME}" +GO_BUILD_FLAGS=-ldflags " \ + -X $(APP_CFG).Version=${CURR_VERSION} \ + -X $(APP_CFG).CommitSha=${COMMIT_SHA} \ + -X $(APP_CFG).BuildTime=${BUILD_TIME} \ +" GO_BUILD_PREFIX=$(TARGET_DIR)/$(BINARY_NAME)-${CURR_VERSION} # If commit sha is not available try git @@ -35,13 +39,24 @@ TARGET_DIR=target # Output directory for the zipped artifacts OUTPUT_DIR=output -# Build commands for each platform -PLATFORMS := darwin-amd64 darwin-arm64 linux-amd64 linux-arm linux-arm64 windows-amd64-.exe windows-arm-.exe windows-arm64-.exe +# Build commands for each platform (extra hyphen used in windows to avoid issues with the .exe extension) +PLATFORMS := \ + darwin-amd64 \ + darwin-arm64 \ + linux-amd64 \ + linux-arm \ + linux-arm64 \ + windows-amd64-.exe \ + windows-arm-.exe \ + windows-arm64-.exe build: test clean $(addprefix build-,$(PLATFORMS)) zip-builds verify-checksums build-%: - GOOS=$(word 1,$(subst -, ,$*)) GOARCH=$(word 2,$(subst -, ,$*)) go build $(GO_BUILD_FLAGS) -o $(GO_BUILD_PREFIX)-$(word 1,$(subst -, ,$*))-$(word 2,$(subst -, ,$*))$(word 3,$(subst -, ,$*)) + GOOS=$(word 1,$(subst -, ,$*)) \ + GOARCH=$(word 2,$(subst -, ,$*)) \ + go build $(GO_BUILD_FLAGS) \ + -o $(GO_BUILD_PREFIX)-$(word 1,$(subst -, ,$*))-$(word 2,$(subst -, ,$*))$(word 3,$(subst -, ,$*)) zip-builds: ./.github/scripts/zip-builds.sh $(BINARY_NAME)-$(CURR_VERSION) $(TARGET_DIR) $(OUTPUT_DIR) @@ -59,6 +74,21 @@ run: test: go test -v ./... +.PHONY: build-test +build-test: + go build \ + -ldflags "\ + -X $(APP_CFG).TestMode=true \ + -X $(APP_CFG).Version=${CURR_VERSION}-testbuild \ + -X $(APP_CFG).CommitSha=${COMMIT_SHA} \ + -X $(APP_CFG).BuildTime=${BUILD_TIME} \ + " \ + -o $(BINARY_NAME)_testbuild + +.PHONY: test-bats +test-bats: build-test + bats ./tests + # Target for cleaning up the target directory .PHONY: clean clean: diff --git a/README.md b/README.md index dfd4e6f7..f987cc73 100644 --- a/README.md +++ b/README.md @@ -54,30 +54,25 @@ CLI via the `man.Docs.GetDoc()` function. ## Testing -The [tests](./tests) directory contains e2e Bash Automated Test System (bats) tests for all of the cli functionality. - -To install bats on MacOS: -``` -$ brew install bats-core -``` -Or with NPM on any OS: -``` -# To install globally: -$ npm install -g bats - -# To install into your project and save it as one of the "devDependencies" in -# your package.json: -$ npm install --save-dev bats -``` - -These tests require the platform to be running and provisioned with basic keycloak clients/users. Before running, clone https://github.com/opentdf/platform and follow [the quickstart](https://github.com/opentdf/platform?tab=readme-ov-file#quick-start) to spin it up. - -Build the cli: -``` -$ go build . -``` - -Run the bats with: -``` -$ bats tests/*.bats -``` +The CLI is equipped with a test mode that can be enabled by building the CLI with `config.TestMode = true`. +For convenience, the CLI can be built with `make build-test`. + +**Test Mode features**: + +- Use the in-memory keyring provider for user profiles +- Enable provisioning profiles for testing via `OTDFCTL_TEST_PROFILE` environment variable + +### BATS + +> [!NOTE] +> Bat Automated Test System (bats) is a TAP-compliant testing framework for Bash. It provides a simple way to verify that the UNIX programs you write behave as expected. + +BATS is used to test the CLI from an end-to-end perspective. To run the tests you will need to ensure the following +pre-requisites are met: + +- bats is installed on your system + - MacOS: `brew install bats-core bats-support bats-assert` +- The platform is running and provisioned with basic keycloak clients/users + - 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`. diff --git a/pkg/config/config.go b/pkg/config/config.go index 8cd410fa..46dcf66c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -11,14 +11,20 @@ import ( "github.com/spf13/viper" ) -// AppName is the name of the application -// Note: use caution when renaming as it is used in various places within the CLI including for config file naming -// and in the profile store -var AppName = "otdfctl" - -var Version = "0.0.0" -var BuildTime = "1970-01-01T00:00:00Z" -var CommitSha = "0000000" +var ( + // AppName is the name of the application + // Note: use caution when renaming as it is used in various places within the CLI including for + // config file naming and in the profile store + AppName = "otdfctl" + + Version = "0.0.0" + BuildTime = "1970-01-01T00:00:00Z" + CommitSha = "0000000" + + // Test mode is used to determine if the application is running in test mode + // "true" = running in test mode + TestMode = "" +) type Output struct { Format string `yaml:"format" default:"styled"` diff --git a/pkg/profiles/profile.go b/pkg/profiles/profile.go index e62379e2..eac61bac 100644 --- a/pkg/profiles/profile.go +++ b/pkg/profiles/profile.go @@ -67,6 +67,10 @@ func newStoreFactory(driver string) NewStoreInterface { func New(opts ...profileConfigVariadicFunc) (*Profile, error) { var err error + if testProfile != nil { + return testProfile, nil + } + config := profileConfig{ driver: PROFILE_DRIVER_DEFAULT, } diff --git a/pkg/profiles/storeKeyring.go b/pkg/profiles/storeKeyring.go index dcf1d504..51827fd9 100644 --- a/pkg/profiles/storeKeyring.go +++ b/pkg/profiles/storeKeyring.go @@ -7,8 +7,6 @@ import ( "github.com/zalando/go-keyring" ) -// TODO: update the store to use alternative storage methods besides keyring - type KeyringStore struct { namespace string key string diff --git a/pkg/profiles/test.go b/pkg/profiles/test.go new file mode 100644 index 00000000..f5af8cca --- /dev/null +++ b/pkg/profiles/test.go @@ -0,0 +1,74 @@ +package profiles + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + + "github.com/opentdf/otdfctl/pkg/config" + "github.com/zalando/go-keyring" +) + +const testModeMsg = ` +******************** +RUNNING IN TEST MODE + +test config: %s +******************** + +` + +var ( + testProfile *Profile + testCfg = os.Getenv("OTDFCTL_TEST_PROFILE") +) + +type testConfig struct { + // global config is used to get the store in a bad state + GlobalConfig config.Config `json:"globalConfig,omitempty"` + + // set the default profile + DefaultProfile string `json:"defaultProfile,omitempty"` + + // profiles to add + Profiles []ProfileConfig `json:"profiles,omitempty"` +} + +func init() { + // If running in test mode, use the mock keyring + if config.TestMode == "true" { + fmt.Printf(testModeMsg, testCfg) + + keyring.MockInit() + + // configure the keyring based on the test config + // unmarsal the test config + if testCfg != "" { + var err error + var cfg testConfig + if err := json.NewDecoder(bytes.NewReader([]byte(testCfg))).Decode(&cfg); err != nil { + panic(err) + } + + testProfile, err = New() + if err != nil { + panic(err) + } + + for _, p := range cfg.Profiles { + err := testProfile.AddProfile(p.Name, p.Endpoint, p.TlsNoVerify, cfg.DefaultProfile == p.Name) + if err != nil { + panic(err) + } + } + + // set default + if cfg.DefaultProfile != "" { + if err := testProfile.SetDefaultProfile(cfg.DefaultProfile); err != nil { + panic(err) + } + } + } + } +} diff --git a/tests/profile.bats b/tests/profile.bats new file mode 100644 index 00000000..645985fe --- /dev/null +++ b/tests/profile.bats @@ -0,0 +1,134 @@ +#!/usr/bin/env bats + +setup() { + bats_require_minimum_version 1.5.0 + + OTDFCTL_BIN=./otdfctl_testbuild + + # Check if BATS_SUPPORT_PATH environment variable exists + if [ -z "${BATS_SUPPORT_PATH}" ]; then + FINAL_BATS_SUPPORT_PATH="$(brew --prefix)/lib" + else + FINAL_BATS_SUPPORT_PATH="${BATS_SUPPORT_PATH}" + fi + echo "FINAL_BATS_SUPPORT_PATH: $FINAL_BATS_SUPPORT_PATH" + load "${FINAL_BATS_SUPPORT_PATH}/bats-support/load.bash" + load "${FINAL_BATS_SUPPORT_PATH}/bats-assert/load.bash" + + set_test_profile() { + auth="" + # if 3rd argument is empty, then don't include it + if [ -n "$3" ]; then + auth=",\"auth\":$3" + fi + echo "{\"profile\":\"$1\",\"endpoint\":\"$2\"$auth}" + } + + set_test_profile_auth() { + authType=$1 + clientId=$2 + clientSecret=$3 + accessToken=$4 + echo "{\"authType\":\"$authType\",\"clientId\":\"$clientId\",\"clientSecret\":\"$clientSecret\",\"accessToken\":\"$accessToken\"}" + } + + set_test_profile_auth_access_token() { + publicClientId=$1 + accessToken=$2 + refreshToken=$3 + expiration=$4 + echo "{\"publicClientId\":\"$publicClientId\",\"accessToken\":\"$accessToken\",\"refreshToken\":\"$refreshToken\",\"expiration\":\"$expiration\"}" + } + + set_test_config() { + defaultProfile=$1 + shift 1 + profiles="" + for i in "$@"; do + # if first profile just set it + if [ -z "$profiles" ]; then + profiles="$i" + else + profiles="$profiles,$i" + fi + done + export OTDFCTL_TEST_PROFILE="{\"defaultProfile\":\"$defaultProfile\",\"profiles\":[$profiles]}" + } + + run_otdfctl() { + run sh -c "./$OTDFCTL_BIN $*" + } + + assert_no_profile_set() { + assert_output --partial "No default profile set" + } + + # Set the keyring provider to in-memory + export OTDFCTL_KEYRING_PROVIDER="in-memory" +} + +teardown() { + unset OTDFCTL_KEYRING_PROVIDER +} + +@test "profile create" { + run_otdfctl profile create test http://localhost:8080 + assert_output --regexp "Creating profile .* ok" + + run_otdfctl profile create test localhost:8080 + assert_output --regexp "Failed .* invalid scheme" + + # TODO figure out how to test the case where the profile already exists +} + +@test "profile list" { + run_otdfctl profile list + assert_no_profile_set + + # export OTDFCTL_TEST_CONFIG='{"defaultProfile":"test","profiles":[{"profile": "test","endpoint":"http://localhost:8080"}]}' + set_test_config "test2" $(set_test_profile "test" "http://localhost:8080") $(set_test_profile "test2" "http://localhost:8081") + run_otdfctl profile list + assert_line --index 5 --regexp "test$" + assert_line --index 6 --regexp "\* test2$" +} + +@test "profile get" { + run_otdfctl profile get test + assert_no_profile_set + + set_test_config "test2" $(set_test_profile "test" "http://localhost:8080") $(set_test_profile "test2" "http://localhost:8081") + run_otdfctl profile get test + assert_line --index 8 --regexp "Profile\s+|\s*test\s*" + assert_output --regexp "Endpoint\s+|\s*http://localhost:8080" + assert_output --regexp "default\s+|\s*false" + # TODO check auth +} + +@test "profile delete" { + run_otdfctl profile delete test + assert_no_profile_set + + # TODO test deleting the default profile + + set_test_config "test2" $(set_test_profile "test" "http://localhost:8080") $(set_test_profile "test2" "http://localhost:8081") + run_otdfctl profile delete test + assert_output --partial "Deleting profile test... ok" +} + +@test "profile set-default" { + run_otdfctl profile set-default test + assert_no_profile_set + + set_test_config "test2" $(set_test_profile "test" "http://localhost:8080") $(set_test_profile "test2" "http://localhost:8081") + run_otdfctl profile set-default test + assert_output --partial "Setting profile test as default... ok" +} + +@test "profile set-endpoint" { + run_otdfctl profile set-endpoint test http://localhost:8081 + assert_no_profile_set + + set_test_config "test2" $(set_test_profile "test" "http://localhost:8080") $(set_test_profile "test2" "http://localhost:8081") + run_otdfctl profile set-endpoint test http://localhost:8081 + assert_output --partial "Setting endpoint for profile test... ok" +} \ No newline at end of file