From c0a7f7a1ae351c5f06a43310dbb23ce1f056038c Mon Sep 17 00:00:00 2001 From: Ben Burkert Date: Sun, 14 Apr 2024 20:42:30 -0400 Subject: [PATCH] v0.0.22: windows msi & chocolatey support --- .github/workflows/release.yml | 111 +++++++++++++++++- auth/auth_test.go | 9 +- auth/signin_test.go | 6 +- auth/signout_test.go | 3 +- auth/whoami_test.go | 3 +- cli.go | 10 +- cmd.go | 66 ++++++++++- cmd/anchor/.goreleaser.yaml | 93 ++++++++++++--- cmd/anchor/main.go | 21 +++- cmd/anchor/windows/als2.ico | Bin 0 -> 98886 bytes cmd/anchor/windows/app.wxs | 37 ++++++ cmdtest/cmdtest.go | 26 +++- lcl/audit.go | 2 +- lcl/audit_test.go | 7 +- lcl/clean.go | 7 +- lcl/clean_test.go | 13 +- lcl/config.go | 9 ++ lcl/config_test.go | 56 ++++++++- lcl/lcl.go | 12 ++ lcl/lcl_test.go | 80 ++++++++++++- lcl/mkcert.go | 15 ++- lcl/mkcert_test.go | 34 +++++- lcl/setup.go | 9 ++ lcl/setup_test.go | 22 ++++ lcl/testdata/TestCmdDefLclAudit/basics.golden | 27 ----- lcl/testdata/TestCmdLcl/--help.golden | 12 +- .../--help.golden | 0 lcl/testdata/TestCmdLclClean/--help.golden | 7 ++ lcl/testdata/TestCmdLclConfig/--help.golden | 8 ++ lcl/testdata/TestCmdLclMkCert/--help.golden | 9 ++ lcl/testdata/TestCmdLclSetup/--help.golden | 8 ++ testdata/TestCmdRoot/--help.golden | 2 + testdata/TestCmdRoot/root.golden | 2 + trust/audit.go | 11 ++ trust/audit_test.go | 52 ++++++++ trust/clean.go | 15 +++ trust/clean_test.go | 103 ++++++++++++++++ trust/testdata/TestClean/basics.golden | 17 +++ trust/testdata/TestCmdTrust/--help.golden | 22 ++++ .../testdata/TestCmdTrustAudit/--help.golden | 19 +++ .../testdata/TestCmdTrustClean/--help.golden | 12 ++ trust/trust.go | 12 ++ trust/trust_test.go | 72 ++++++++++++ version/command.go | 5 + version/command_test.go | 15 ++- version/testdata/TestCmdVersion/--help.golden | 7 ++ 46 files changed, 991 insertions(+), 97 deletions(-) create mode 100644 cmd/anchor/windows/als2.ico create mode 100644 cmd/anchor/windows/app.wxs delete mode 100644 lcl/testdata/TestCmdDefLclAudit/basics.golden rename lcl/testdata/{TestCmdDefLclAudit => TestCmdLclAudit}/--help.golden (100%) create mode 100644 lcl/testdata/TestCmdLclClean/--help.golden create mode 100644 lcl/testdata/TestCmdLclConfig/--help.golden create mode 100644 lcl/testdata/TestCmdLclMkCert/--help.golden create mode 100644 lcl/testdata/TestCmdLclSetup/--help.golden create mode 100644 trust/clean_test.go create mode 100644 trust/testdata/TestClean/basics.golden create mode 100644 trust/testdata/TestCmdTrust/--help.golden create mode 100644 trust/testdata/TestCmdTrustAudit/--help.golden create mode 100644 trust/testdata/TestCmdTrustClean/--help.golden create mode 100644 version/testdata/TestCmdVersion/--help.golden diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0338f00..e2cadef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,13 +3,70 @@ name: Release concurrency: release on: + workflow_dispatch: push: - branches: - - main + tags: ["v*"] + +permissions: + contents: write + packages: write + id-token: write jobs: + prepare: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + - shell: bash + run: | + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + - uses: actions/cache@v4 + if: matrix.os == 'ubuntu-latest' + with: + path: cmd/anchor/dist/linux + key: linux-${{ env.sha_short }} + - uses: actions/cache@v4 + if: matrix.os == 'macos-latest' + with: + path: cmd/anchor/dist/darwin + key: darwin-${{ env.sha_short }} + - uses: actions/cache@v4 + if: matrix.os == 'windows-latest' + with: + path: cmd/anchor/dist/windows + key: windows-${{ env.sha_short }} + enableCrossOsArchive: true + - name: non-windows flags + if: matrix.os != 'windows-latest' + shell: bash + run: echo 'flags=--skip chocolatey' >> $GITHUB_ENV + - name: windows flags + if: matrix.os == 'windows-latest' + shell: bash + run: echo 'flags=--skip homebrew' >> $GITHUB_ENV + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser-pro + version: latest + args: release --clean --split ${{ env.flags }} + workdir: cmd/anchor + env: + GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} release: runs-on: ubuntu-latest + needs: prepare steps: - name: Checkout uses: actions/checkout@v3 @@ -19,13 +76,61 @@ jobs: uses: actions/setup-go@v4 with: go-version-file: go.mod + + # Copy Caches + - shell: bash + run: | + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + path: cmd/anchor/dist/linux + key: linux-${{ env.sha_short }} + - uses: actions/cache@v4 + with: + path: cmd/anchor/dist/darwin + key: darwin-${{ env.sha_short }} + - uses: actions/cache@v4 + with: + path: cmd/anchor/dist/windows + key: windows-${{ env.sha_short }} + enableCrossOsArchive: true + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: distribution: goreleaser-pro version: latest - args: release --clean + args: continue --merge workdir: cmd/anchor env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + + publish-windows: + runs-on: windows-latest + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Copy Caches + - shell: bash + run: | + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + path: cmd/anchor/dist/windows + key: windows-${{ env.sha_short }} + enableCrossOsArchive: true + - shell: bash + run: | + cp cmd/anchor/dist/windows/anchor.${{ env.RELEASE_VERSION }}.nupkg ./ + + - shell: pwsh + run: | + choco push --source https://push.chocolatey.org/ --api-key "$env:CHOCOLATEY_API_KEY" anchor.${{ env.RELEASE_VERSION }}.nupkg + env: + CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }} diff --git a/auth/auth_test.go b/auth/auth_test.go index 51ad217..1c472e1 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -5,6 +5,7 @@ import ( "flag" "testing" + "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api/apitest" "github.com/anchordotdev/cli/cmdtest" ) @@ -27,13 +28,15 @@ func TestMain(m *testing.M) { func TestCmdAuth(t *testing.T) { cmd := CmdAuth - root := cmd.Root() + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true t.Run("auth", func(t *testing.T) { - cmdtest.TestOutput(t, root, "auth") + cfg.Test.SkipRunE = false + cmdtest.TestOutput(t, cmd, "auth") }) t.Run("--help", func(t *testing.T) { - cmdtest.TestOutput(t, root, "auth", "--help") + cmdtest.TestOutput(t, cmd, "auth", "--help") }) } diff --git a/auth/signin_test.go b/auth/signin_test.go index f77160d..7004e5b 100644 --- a/auth/signin_test.go +++ b/auth/signin_test.go @@ -3,15 +3,17 @@ package auth import ( "testing" + "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/cmdtest" ) func TestCmdAuthSignin(t *testing.T) { cmd := CmdAuthSignin - root := cmd.Root() + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true t.Run("--help", func(t *testing.T) { - cmdtest.TestOutput(t, root, "auth", "signin", "--help") + cmdtest.TestOutput(t, cmd, "auth", "signin", "--help") }) } diff --git a/auth/signout_test.go b/auth/signout_test.go index 4df2041..c8bf46f 100644 --- a/auth/signout_test.go +++ b/auth/signout_test.go @@ -13,10 +13,9 @@ func TestCmdAuthSignout(t *testing.T) { cmd := CmdAuthSignin cfg := cli.ConfigFromCmd(cmd) cfg.Test.SkipRunE = true - root := cmd.Root() t.Run("--help", func(t *testing.T) { - cmdtest.TestOutput(t, root, "auth", "signout", "--help") + cmdtest.TestOutput(t, cmd, "auth", "signout", "--help") }) } diff --git a/auth/whoami_test.go b/auth/whoami_test.go index 54f0cea..cfb5704 100644 --- a/auth/whoami_test.go +++ b/auth/whoami_test.go @@ -16,10 +16,9 @@ func TestCmdAuthWhoAmI(t *testing.T) { cmd := CmdAuthWhoami cfg := cli.ConfigFromCmd(cmd) cfg.Test.SkipRunE = true - root := cmd.Root() t.Run("--help", func(t *testing.T) { - cmdtest.TestOutput(t, root, "auth", "whoami", "--help") + cmdtest.TestOutput(t, cmd, "auth", "whoami", "--help") }) } diff --git a/cli.go b/cli.go index db86cee..498e534 100644 --- a/cli.go +++ b/cli.go @@ -38,16 +38,12 @@ type Config struct { } `cmd:"config"` MkCert struct { - Domains string `flag:"domains"` - SubCa string `flag:"subca"` + Domains []string `flag:"domains"` + SubCa string `flag:"subca"` } `cmd:"mkcert"` Setup struct { - PackageManager string `desc:"Package manager to use for integrating Anchor." flag:"package-manager" env:"PACKAGE_MANAGER" json:"package_manager" toml:"package-manager"` - Service string `desc:"Name for lcl.host service." flag:"service" env:"SERVICE" json:"service" toml:"service"` - Subdomain string `desc:"Subdomain for lcl.host service." flag:"subdomain" env:"SUBDOMAIN" json:"subdomain" toml:"subdomain"` - File string `desc:"File Anchor should use to detect package manager." flag:"file" env:"PACKAGE_MANAGER_FILE" json:"file" toml:"file"` - Language string `desc:"Language to use for integrating Anchor." flag:"language" json:"language" toml:"language"` + Language string `desc:"Language to use for integrating Anchor." flag:"language" json:"language" toml:"language"` } `cmd:"setup"` } `cmd:"lcl"` diff --git a/cmd.go b/cmd.go index 52fe5cf..4c0c8c5 100644 --- a/cmd.go +++ b/cmd.go @@ -77,7 +77,7 @@ var rootDef = CmdDef{ { Name: "lcl", - Use: "lcl [flags]", + Use: "lcl [flags]", Short: "Manage lcl.host Local Development Environment", SubDefs: []CmdDef{ @@ -87,13 +87,77 @@ var rootDef = CmdDef{ Use: "audit [flags]", Short: "Audit lcl.host HTTPS Local Development Environment", }, + { + Name: "clean", + + Use: "clean [flags]", + Short: "Clean lcl.host CA Certificates from the Local Trust Store(s)", + }, + { + Name: "config", + + Use: "config [flags]", + Short: "Configure System for lcl.host Local Development", + }, + { + Name: "mkcert", + + Use: "mkcert [flags]", + Short: "Provision Certificate for lcl.host Local Development", + }, + { + Name: "setup", + + Use: "setup [flags]", + Short: "Setup lcl.host Application", + }, }, }, { Name: "trust", + + Use: "trust [flags]", + Short: "Manage CA Certificates in your Local Trust Store(s)", + Long: heredoc.Doc(` + Install the AnchorCA certificates of a target organization, realm, or CA into + your local system's trust store. The default target is the localhost realm of + your personal organization. + + After installation of the AnchorCA certificates, Leaf certificates under the + AnchorCA certificates will be trusted by browsers and programs on your system. + `), + SubDefs: []CmdDef{ + { + Name: "audit", + + Use: "audit [flags]", + Short: "Audit CA Certificates in your Local Trust Store(s)", + Long: heredoc.Doc(` + Perform an audit of the local trust store(s) and report any expected, missing, + or extra CA certificates per store. A set of expected CAs is fetched for the + target org and (optional) realm. The default stores to audit are system, nss, + and homebrew. + + CA certificate states: + + * VALID: an expected CA certificate is present in every trust store. + * MISSING: an expected CA certificate is missing in one or more stores. + * EXTRA: an unexpected CA certificate is present in one or more stores. + `), + }, + { + Name: "clean", + + Use: "clean [flags]", + Short: "Clean CA Certificates from your Local Trust Store(s)", + }, + }, }, { Name: "version", + + Use: "version", + Short: "Show version info", }, }, } diff --git a/cmd/anchor/.goreleaser.yaml b/cmd/anchor/.goreleaser.yaml index 74e8ef8..9293e81 100644 --- a/cmd/anchor/.goreleaser.yaml +++ b/cmd/anchor/.goreleaser.yaml @@ -4,26 +4,48 @@ before: hooks: - go mod tidy +variables: + windows_product_guid: C5F94F62-E3CC-48E2-AB3C-4DED8C6099E9 + windows_upgrade_code: A7CF6DDC-58DA-4A29-94C7-3D46C9BE0944 + windows_installer_version: 100 + builds: - - env: - - CGO_ENABLED=0 - goos: - - linux - - windows - - darwin + - <<: &build_defaults + env: + - CGO_ENABLED=0 + id: linux + goos: [linux] + goarch: [amd64,arm64,386] + - <<: *build_defaults + id: macos + goos: [darwin] + goarch: [amd64,arm64] + - <<: *build_defaults + id: windows + goos: [windows] + goarch: [amd64,386] archives: - - format: tar.gz - name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - format_overrides: - - goos: windows - format: zip + - id: linux + builds: [linux] + format: tar.gz + <<: &archive_defaults + wrap_in_directory: true + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + - <<: *archive_defaults + id: macos + builds: [macos] + format: zip + - <<: *archive_defaults + id: windows + builds: [windows] + format: zip brews: - repository: @@ -31,9 +53,44 @@ brews: owner: anchordotdev description: Command-line tools for Anchor.dev - folder: Formula + directory: Formula homepage: https://anchor.dev/ license: MIT test: | assert_match "anchor is a command line interface for the Anchor certificate management platform.", shell_output("#{bin}/anchor") + +msi: +- id: anchor-msi + ids: + - windows + wxs: ./windows/app.wxs + mod_timestamp: "{{ .CommitTimestamp }}" + +chocolateys: +- name: anchor + ids: + - windows + - anchor-msi + package_source_url: https://github.com/anchordotdev/cli + owners: Anchor Security, Inc. + title: Anchor CLI + authors: Anchor + project_url: https://anchor.dev + use: msi + copyright: 2024 Anchor Security, Inc. + license_url: https://raw.githubusercontent.com/anchordotdev/cli/main/LICENSE + require_license_acceptance: false + project_source_url: https://github.com/anchordotdev/cli + docs_url: https://anchor.dev/docs + bug_tracker_url: https://github.com/anchordotdev/cli/issues + icon_url: https://anchor.dev/images/als2.png + tags: "security tls ssl certificates localhost https cryptography encryption acme cli x509 X.509" + summary: "Command-line tools for Anchor.dev" + description: | + anchor is a command line interface for the Anchor certificate management platform. + It provides a developer friendly interface for certificate management. + release_notes: "https://github.com/anchordotdev/cli/releases/tag/v{{ .Version }}" + api_key: "{{ .Env.CHOCOLATEY_API_KEY }}" + source_repo: "https://push.chocolatey.org/" + skip_publish: true \ No newline at end of file diff --git a/cmd/anchor/main.go b/cmd/anchor/main.go index 972dfb6..e28e7ee 100644 --- a/cmd/anchor/main.go +++ b/cmd/anchor/main.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "runtime" "time" "github.com/MakeNowJust/heredoc" @@ -214,13 +215,21 @@ func versionCheck(ctx context.Context) error { if release.TagName == nil || *release.TagName != "v"+version { fmt.Println(ui.StepHint("A new release of the anchor CLI is available.")) - command := "brew update && brew upgrade anchor" - if err := clipboard.WriteAll(command); err == nil { - fmt.Println(ui.StepAlert(fmt.Sprintf("Copied %s to your clipboard.", ui.Announce(command)))) + if !isWindowsRuntime() { + command := "brew update && brew upgrade anchor" + if err := clipboard.WriteAll(command); err == nil { + fmt.Println(ui.StepAlert(fmt.Sprintf("Copied %s to your clipboard.", ui.Announce(command)))) + } + fmt.Println(ui.StepAlert(fmt.Sprintf("%s `%s` to update to the latest version.", ui.Action("Run"), ui.Emphasize(command)))) + fmt.Println(ui.StepHint(fmt.Sprintf("Not using homebrew? Explore other options here: %s", ui.URL("https://github.com/anchordotdev/cli")))) + fmt.Println() + } else { + // TODO(amerine): Add chocolatey instructions. } - fmt.Println(ui.StepAlert(fmt.Sprintf("%s `%s` to update to the latest version.", ui.Action("Run"), ui.Emphasize(command)))) - fmt.Println(ui.StepHint(fmt.Sprintf("Not using homebrew? Explore other options here: %s", ui.URL("https://github.com/anchordotdev/cli")))) - fmt.Println() } return nil } + +func isWindowsRuntime() bool { + return os.Getenv("GOOS") == "windows" || runtime.GOOS == "windows" +} diff --git a/cmd/anchor/windows/als2.ico b/cmd/anchor/windows/als2.ico new file mode 100644 index 0000000000000000000000000000000000000000..30241ed17be1515f981fe5d8f3f622ee6bcf24da GIT binary patch literal 98886 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 literal 0 HcmV?d00001 diff --git a/cmd/anchor/windows/app.wxs b/cmd/anchor/windows/app.wxs new file mode 100644 index 0000000..a883c22 --- /dev/null +++ b/cmd/anchor/windows/app.wxs @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmdtest/cmdtest.go b/cmdtest/cmdtest.go index 3fadbd2..eff179b 100644 --- a/cmdtest/cmdtest.go +++ b/cmdtest/cmdtest.go @@ -5,20 +5,36 @@ import ( "io" "testing" + "github.com/anchordotdev/cli" "github.com/charmbracelet/x/exp/teatest" + "github.com/joeshaw/envdecode" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) -func TestOutput(t *testing.T, cmd *cobra.Command, args ...string) { +func TestExecute(t *testing.T, cmd *cobra.Command, args ...string) *bytes.Buffer { + // pull from env again, since setting in tests is too late for normal env handling + cfg := cli.ConfigFromCmd(cmd) + if err := envdecode.Decode(cfg); err != nil && err != envdecode.ErrNoTargetFieldsAreSet { + panic(err) + } + + root := cmd.Root() + b := new(bytes.Buffer) - cmd.SetErr(b) - cmd.SetOut(b) - cmd.SetArgs(args) + root.SetErr(b) + root.SetOut(b) + root.SetArgs(args) - err := cmd.Execute() + err := root.Execute() require.NoError(t, err) + return b +} + +func TestOutput(t *testing.T, cmd *cobra.Command, args ...string) { + b := TestExecute(t, cmd, args...) + out, err := io.ReadAll(b) if err != nil { t.Fatal(err) diff --git a/lcl/audit.go b/lcl/audit.go index 1db5817..ffc26a9 100644 --- a/lcl/audit.go +++ b/lcl/audit.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -var CmdAuthLclAudit = cli.NewCmd[Audit](CmdLcl, "audit", func(cmd *cobra.Command) { +var CmdLclAudit = cli.NewCmd[Audit](CmdLcl, "audit", func(cmd *cobra.Command) { cmd.Args = cobra.NoArgs }) diff --git a/lcl/audit_test.go b/lcl/audit_test.go index fafa71f..cd73ae6 100644 --- a/lcl/audit_test.go +++ b/lcl/audit_test.go @@ -10,14 +10,13 @@ import ( "github.com/anchordotdev/cli/ui/uitest" ) -func TestCmdDefLclAudit(t *testing.T) { - cmd := CmdAuthLclAudit +func TestCmdLclAudit(t *testing.T) { + cmd := CmdLclAudit cfg := cli.ConfigFromCmd(cmd) cfg.Test.SkipRunE = true - root := cmd.Root() t.Run("--help", func(t *testing.T) { - cmdtest.TestOutput(t, root, "lcl", "audit", "--help") + cmdtest.TestOutput(t, cmd, "lcl", "audit", "--help") }) } diff --git a/lcl/clean.go b/lcl/clean.go index f99e0f9..6647aef 100644 --- a/lcl/clean.go +++ b/lcl/clean.go @@ -9,8 +9,13 @@ import ( "github.com/anchordotdev/cli/lcl/models" "github.com/anchordotdev/cli/trust" "github.com/anchordotdev/cli/ui" + "github.com/spf13/cobra" ) +var CmdLclClean = cli.NewCmd[LclClean](CmdLcl, "clean", func(cmd *cobra.Command) { + cmd.Args = cobra.NoArgs +}) + type LclClean struct { anc *api.Session orgSlug, realmSlug string @@ -23,7 +28,7 @@ func (c LclClean) UI() cli.UI { } func (c LclClean) run(ctx context.Context, drv *ui.Driver) error { - cfg := cli.ConfigFromContext(ctx) + cfg := cli.ConfigFromContext(ctx) var err error clientCmd := &auth.Client{ diff --git a/lcl/clean_test.go b/lcl/clean_test.go index 1dc1859..d929513 100644 --- a/lcl/clean_test.go +++ b/lcl/clean_test.go @@ -5,9 +5,20 @@ import ( "testing" "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/ui/uitest" ) +func TestCmdLclClean(t *testing.T) { + cmd := CmdLclClean + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "lcl", "clean", "--help") + }) +} + func TestClean(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -21,7 +32,7 @@ func TestClean(t *testing.T) { if cfg.API.Token, err = srv.GeneratePAT("anky@anchor.dev"); err != nil { t.Fatal(err) } - ctx = cli.ContextWithConfig(ctx, cfg) + ctx = cli.ContextWithConfig(ctx, cfg) t.Run("basics", func(t *testing.T) { if srv.IsProxy() { diff --git a/lcl/config.go b/lcl/config.go index d25236a..453a731 100644 --- a/lcl/config.go +++ b/lcl/config.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/cli/browser" + "github.com/spf13/cobra" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api" @@ -21,6 +22,14 @@ import ( "github.com/anchordotdev/cli/ui" ) +var CmdLclConfig = cli.NewCmd[LclConfig](CmdLcl, "config", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + + cmd.Args = cobra.NoArgs + + cmd.Flags().StringVarP(&cfg.Lcl.DiagnosticAddr, "addr", "a", ":4433", "Address for local diagnostic web server.") +}) + var loopbackAddrs = []string{"127.0.0.1", "::1"} type LclConfig struct { diff --git a/lcl/config_test.go b/lcl/config_test.go index 1cdc704..8afd06e 100644 --- a/lcl/config_test.go +++ b/lcl/config_test.go @@ -12,12 +12,66 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/exp/teatest" + "github.com/stretchr/testify/require" "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/truststore" "github.com/anchordotdev/cli/ui/uitest" ) +func TestCmdLclConfig(t *testing.T) { + cmd := CmdLclConfig + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "lcl", "config", "--help") + }) + + t.Run("default --addr", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.DiagnosticAddr = ":4433" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "config") + + require.Equal(t, ":4433", cfg.Lcl.DiagnosticAddr) + }) + + t.Run("-a :4444", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.DiagnosticAddr = ":4433" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "config", "-a", ":4444") + + require.Equal(t, ":4444", cfg.Lcl.DiagnosticAddr) + }) + + t.Run("--addr :4455", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.DiagnosticAddr = ":4433" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "config", "--addr", ":4455") + + require.Equal(t, ":4455", cfg.Lcl.DiagnosticAddr) + }) + + t.Run("ADDR=:4466", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.DiagnosticAddr = ":4433" + }) + + t.Setenv("ADDR", ":4466") + + cmdtest.TestExecute(t, cmd, "lcl", "config") + + require.Equal(t, ":4466", cfg.Lcl.DiagnosticAddr) + }) +} + func TestLclConfig(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -41,7 +95,7 @@ func TestLclConfig(t *testing.T) { if cfg.API.Token, err = srv.GeneratePAT("lcl_config@anchor.dev"); err != nil { t.Fatal(err) } - ctx = cli.ContextWithConfig(ctx, cfg) + ctx = cli.ContextWithConfig(ctx, cfg) t.Run("basics", func(t *testing.T) { ctx, cancel := context.WithCancel(ctx) diff --git a/lcl/lcl.go b/lcl/lcl.go index 70a23b3..a9d382d 100644 --- a/lcl/lcl.go +++ b/lcl/lcl.go @@ -19,7 +19,19 @@ import ( ) var CmdLcl = cli.NewCmd[Command](cli.CmdRoot, "lcl", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + cmd.Args = cobra.NoArgs + + // config + cmd.Flags().StringVarP(&cfg.Lcl.DiagnosticAddr, "addr", "a", ":4433", "Address for local diagnostic web server.") + + // mkcert + cmd.Flags().StringSliceVar(&cfg.Lcl.MkCert.Domains, "domains", []string{}, "Domains to create certificate for.") + cmd.Flags().StringVar(&cfg.Lcl.MkCert.SubCa, "subca", "", "SubCA to create certificate for.") + + // setup + cmd.Flags().StringVar(&cfg.Lcl.Setup.Language, "language", "", "Language to integrate with Anchor.") }) type Command struct { diff --git a/lcl/lcl_test.go b/lcl/lcl_test.go index 65fee3a..fd80df1 100644 --- a/lcl/lcl_test.go +++ b/lcl/lcl_test.go @@ -15,6 +15,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/exp/teatest" + "github.com/stretchr/testify/require" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api/apitest" @@ -42,10 +43,85 @@ func TestMain(m *testing.M) { func TestCmdLcl(t *testing.T) { cmd := CmdLcl - root := cmd.Root() + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true t.Run("--help", func(t *testing.T) { - cmdtest.TestOutput(t, root, "lcl", "--help") + cmdtest.TestOutput(t, cmd, "lcl", "--help") + }) + + // config + + t.Run("default --addr", func(t *testing.T) { + cmdtest.TestExecute(t, cmd, "lcl", "config") + + require.Equal(t, ":4433", cfg.Lcl.DiagnosticAddr) + }) + + t.Run("-a :4444", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.DiagnosticAddr = ":4433" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "-a", ":4444") + + require.Equal(t, ":4444", cfg.Lcl.DiagnosticAddr) + }) + + t.Run("--addr :4455", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.DiagnosticAddr = ":4433" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "--addr", ":4455") + + require.Equal(t, ":4455", cfg.Lcl.DiagnosticAddr) + }) + + t.Run("ADDR=:4466", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.DiagnosticAddr = ":4433" + }) + + t.Setenv("ADDR", ":4466") + + cmdtest.TestExecute(t, cmd, "lcl") + + require.Equal(t, ":4466", cfg.Lcl.DiagnosticAddr) + }) + + // mkcert + + t.Run("--domains test.lcl.host,test.localhost", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.MkCert.Domains = []string{} + }) + + cmdtest.TestExecute(t, cmd, "lcl", "--domains", "test.lcl.host,test.localhost") + + require.Equal(t, []string{"test.lcl.host", "test.localhost"}, cfg.Lcl.MkCert.Domains) + }) + + t.Run("--subca 1234:ABCD:EF123", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.MkCert.SubCa = "" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "--subca", "1234:ABCD:EF123") + + require.Equal(t, "1234:ABCD:EF123", cfg.Lcl.MkCert.SubCa) + }) + + // setup + + t.Run("--language ruby", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.Setup.Language = "" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "--language", "ruby") + + require.Equal(t, "ruby", cfg.Lcl.Setup.Language) }) } diff --git a/lcl/mkcert.go b/lcl/mkcert.go index d573636..c18c303 100644 --- a/lcl/mkcert.go +++ b/lcl/mkcert.go @@ -5,15 +5,24 @@ import ( "crypto/tls" "errors" "net/url" - "strings" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api" "github.com/anchordotdev/cli/auth" "github.com/anchordotdev/cli/cert" "github.com/anchordotdev/cli/ui" + "github.com/spf13/cobra" ) +var CmdLclMkCert = cli.NewCmd[MkCert](CmdLcl, "mkcert", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + + cmd.Args = cobra.NoArgs + + cmd.Flags().StringSliceVar(&cfg.Lcl.MkCert.Domains, "domains", []string{}, "Domains to create certificate for.") + cmd.Flags().StringVar(&cfg.Lcl.MkCert.SubCa, "subca", "", "SubCA to create certificate for.") +}) + type MkCert struct { anc *api.Session @@ -70,9 +79,7 @@ func (c *MkCert) perform(ctx context.Context, drv *ui.Driver) (*tls.Certificate, } if len(c.domains) == 0 { - if cfg.Lcl.MkCert.Domains != "" { - c.domains = strings.Split(cfg.Lcl.MkCert.Domains, ",") - } + c.domains = cfg.Lcl.MkCert.Domains if len(c.domains) == 0 { return nil, errors.New("domains is required") } diff --git a/lcl/mkcert_test.go b/lcl/mkcert_test.go index d6057a6..ef11842 100644 --- a/lcl/mkcert_test.go +++ b/lcl/mkcert_test.go @@ -5,9 +5,41 @@ import ( "testing" "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/ui/uitest" + "github.com/stretchr/testify/require" ) +func TestCmdLclMkCert(t *testing.T) { + cmd := CmdLclMkCert + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "lcl", "mkcert", "--help") + }) + + t.Run("--domains test.lcl.host,test.localhost", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.MkCert.Domains = []string{} + }) + + cmdtest.TestExecute(t, cmd, "lcl", "mkcert", "--domains", "test.lcl.host,test.localhost") + + require.Equal(t, []string{"test.lcl.host", "test.localhost"}, cfg.Lcl.MkCert.Domains) + }) + + t.Run("--subca 1234:ABCD:EF123", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.MkCert.SubCa = "" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "mkcert", "--subca", "1234:ABCD:EF123") + + require.Equal(t, "1234:ABCD:EF123", cfg.Lcl.MkCert.SubCa) + }) +} + func TestLclMkcert(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -23,7 +55,7 @@ func TestLclMkcert(t *testing.T) { if cfg.API.Token, err = srv.GeneratePAT("lcl_mkcert@anchor.dev"); err != nil { t.Fatal(err) } - ctx = cli.ContextWithConfig(ctx, cfg) + ctx = cli.ContextWithConfig(ctx, cfg) t.Run("basics", func(t *testing.T) { t.Skip("pending better support for building needed models before running") diff --git a/lcl/setup.go b/lcl/setup.go index a874196..5e524e0 100644 --- a/lcl/setup.go +++ b/lcl/setup.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/cli/browser" + "github.com/spf13/cobra" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api" @@ -23,6 +24,14 @@ import ( "github.com/anchordotdev/cli/ui" ) +var CmdLclSetup = cli.NewCmd[Setup](CmdLcl, "setup", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + + cmd.Args = cobra.NoArgs + + cmd.Flags().StringVar(&cfg.Lcl.Setup.Language, "language", "", "Language to integrate with Anchor.") +}) + type Setup struct { anc *api.Session orgSlug string diff --git a/lcl/setup_test.go b/lcl/setup_test.go index 2fdc035..c48aecd 100644 --- a/lcl/setup_test.go +++ b/lcl/setup_test.go @@ -8,11 +8,33 @@ import ( "time" "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/ui/uitest" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/exp/teatest" + "github.com/stretchr/testify/require" ) +func TestCmdLclSetup(t *testing.T) { + cmd := CmdLclSetup + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "lcl", "setup", "--help") + }) + + t.Run("--language ruby", func(t *testing.T) { + t.Cleanup(func() { + cfg.Lcl.Setup.Language = "" + }) + + cmdtest.TestExecute(t, cmd, "lcl", "setup", "--language", "ruby") + + require.Equal(t, "ruby", cfg.Lcl.Setup.Language) + }) +} + func TestSetup(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/lcl/testdata/TestCmdDefLclAudit/basics.golden b/lcl/testdata/TestCmdDefLclAudit/basics.golden deleted file mode 100644 index b8af6f0..0000000 --- a/lcl/testdata/TestCmdDefLclAudit/basics.golden +++ /dev/null @@ -1,27 +0,0 @@ -─── Client ───────────────────────────────────────────────────────────────────── - * Checking authentication: probing credentials locally…* -─── Client ───────────────────────────────────────────────────────────────────── - * Checking authentication: testing credentials remotely…* -─── AuditHeader ──────────────────────────────────────────────────────────────── -# Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` -─── AuditHint ────────────────────────────────────────────────────────────────── -# Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` - | We'll begin by checking your system to determine what you need for your setup. -─── AuditResources ───────────────────────────────────────────────────────────── -# Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` - | We'll begin by checking your system to determine what you need for your setup. - * Checking resources on Anchor.dev…* -─── AuditResources ───────────────────────────────────────────────────────────── -# Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` - | We'll begin by checking your system to determine what you need for your setup. - - Checked resources on Anchor.dev: need to provision resources. -─── AuditTrust ───────────────────────────────────────────────────────────────── -# Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` - | We'll begin by checking your system to determine what you need for your setup. - - Checked resources on Anchor.dev: need to provision resources. - * Scanning local and expected CA certificates…* -─── AuditTrust ───────────────────────────────────────────────────────────────── -# Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` - | We'll begin by checking your system to determine what you need for your setup. - - Checked resources on Anchor.dev: need to provision resources. - - Scanned local and expected CA certificates: need to install 2 missing certificates. diff --git a/lcl/testdata/TestCmdLcl/--help.golden b/lcl/testdata/TestCmdLcl/--help.golden index 85ae667..671523a 100644 --- a/lcl/testdata/TestCmdLcl/--help.golden +++ b/lcl/testdata/TestCmdLcl/--help.golden @@ -1,13 +1,21 @@ Manage lcl.host Local Development Environment Usage: - anchor lcl [flags] + anchor lcl [flags] anchor lcl [command] Available Commands: audit Audit lcl.host HTTPS Local Development Environment + clean Clean lcl.host CA Certificates from the Local Trust Store(s) + config Configure System for lcl.host Local Development + mkcert Provision Certificate for lcl.host Local Development + setup Setup lcl.host Application Flags: - -h, --help help for lcl + -a, --addr string Address for local diagnostic web server. (default ":4433") + --domains strings Domains to create certificate for. + -h, --help help for lcl + --language string Language to integrate with Anchor. + --subca string SubCA to create certificate for. Use "anchor lcl [command] --help" for more information about a command. diff --git a/lcl/testdata/TestCmdDefLclAudit/--help.golden b/lcl/testdata/TestCmdLclAudit/--help.golden similarity index 100% rename from lcl/testdata/TestCmdDefLclAudit/--help.golden rename to lcl/testdata/TestCmdLclAudit/--help.golden diff --git a/lcl/testdata/TestCmdLclClean/--help.golden b/lcl/testdata/TestCmdLclClean/--help.golden new file mode 100644 index 0000000..0bdbdad --- /dev/null +++ b/lcl/testdata/TestCmdLclClean/--help.golden @@ -0,0 +1,7 @@ +Clean lcl.host CA Certificates from the Local Trust Store(s) + +Usage: + anchor lcl clean [flags] + +Flags: + -h, --help help for clean diff --git a/lcl/testdata/TestCmdLclConfig/--help.golden b/lcl/testdata/TestCmdLclConfig/--help.golden new file mode 100644 index 0000000..6c48387 --- /dev/null +++ b/lcl/testdata/TestCmdLclConfig/--help.golden @@ -0,0 +1,8 @@ +Configure System for lcl.host Local Development + +Usage: + anchor lcl config [flags] + +Flags: + -a, --addr string Address for local diagnostic web server. (default ":4433") + -h, --help help for config diff --git a/lcl/testdata/TestCmdLclMkCert/--help.golden b/lcl/testdata/TestCmdLclMkCert/--help.golden new file mode 100644 index 0000000..a895139 --- /dev/null +++ b/lcl/testdata/TestCmdLclMkCert/--help.golden @@ -0,0 +1,9 @@ +Provision Certificate for lcl.host Local Development + +Usage: + anchor lcl mkcert [flags] + +Flags: + --domains strings Domains to create certificate for. + -h, --help help for mkcert + --subca string SubCA to create certificate for. diff --git a/lcl/testdata/TestCmdLclSetup/--help.golden b/lcl/testdata/TestCmdLclSetup/--help.golden new file mode 100644 index 0000000..47fbf38 --- /dev/null +++ b/lcl/testdata/TestCmdLclSetup/--help.golden @@ -0,0 +1,8 @@ +Setup lcl.host Application + +Usage: + anchor lcl setup [flags] + +Flags: + -h, --help help for setup + --language string Language to integrate with Anchor. diff --git a/testdata/TestCmdRoot/--help.golden b/testdata/TestCmdRoot/--help.golden index bb88da6..a7e0625 100644 --- a/testdata/TestCmdRoot/--help.golden +++ b/testdata/TestCmdRoot/--help.golden @@ -11,6 +11,8 @@ Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command lcl Manage lcl.host Local Development Environment + trust Manage CA Certificates in your Local Trust Store(s) + version Show version info Flags: -h, --help help for anchor diff --git a/testdata/TestCmdRoot/root.golden b/testdata/TestCmdRoot/root.golden index bb88da6..a7e0625 100644 --- a/testdata/TestCmdRoot/root.golden +++ b/testdata/TestCmdRoot/root.golden @@ -11,6 +11,8 @@ Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command lcl Manage lcl.host Local Development Environment + trust Manage CA Certificates in your Local Trust Store(s) + version Show version info Flags: -h, --help help for anchor diff --git a/trust/audit.go b/trust/audit.go index b422291..5574965 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -7,12 +7,23 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" + "github.com/spf13/cobra" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api" "github.com/anchordotdev/cli/truststore" ) +var CmdTrustAudit = cli.NewCmd[Audit](CmdTrust, "audit", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + + cmd.Args = cobra.NoArgs + + cmd.Flags().StringVarP(&cfg.Trust.Org, "organization", "o", "", "Organization to trust.") + cmd.Flags().StringVarP(&cfg.Trust.Realm, "realm", "r", "", "Realm to trust.") + cmd.Flags().StringSliceVar(&cfg.Trust.Stores, "trust-stores", []string{"homebrew", "nss", "system"}, "Trust stores to update.") +}) + type Audit struct{} func (a Audit) UI() cli.UI { diff --git a/trust/audit_test.go b/trust/audit_test.go index 7f2642a..9392717 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -12,11 +12,63 @@ import ( "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api" "github.com/anchordotdev/cli/api/apitest" + "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/ext509" "github.com/anchordotdev/cli/internal/must" "github.com/anchordotdev/cli/truststore" + "github.com/stretchr/testify/require" ) +func TestCmdTrustAudit(t *testing.T) { + cmd := CmdTrustAudit + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "trust", "audit", "--help") + }) + + t.Run("--organization test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Org = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "audit", "--organization", "test") + + require.Equal(t, "test", cfg.Trust.Org) + }) + + t.Run("-o test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Org = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "audit", "-o", "test") + + require.Equal(t, "test", cfg.Trust.Org) + }) + + t.Run("--realm test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Realm = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "audit", "--realm", "test") + + require.Equal(t, "test", cfg.Trust.Realm) + }) + + t.Run("-r test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Realm = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "audit", "-r", "test") + + require.Equal(t, "test", cfg.Trust.Realm) + }) +} + func TestAudit(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("no pty support on windows") diff --git a/trust/clean.go b/trust/clean.go index 890159f..515fd8b 100644 --- a/trust/clean.go +++ b/trust/clean.go @@ -10,8 +10,23 @@ import ( "github.com/anchordotdev/cli/trust/models" "github.com/anchordotdev/cli/truststore" "github.com/anchordotdev/cli/ui" + "github.com/spf13/cobra" ) +var CmdTrustClean = cli.NewCmd[Clean](CmdTrust, "clean", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + + cmd.Args = cobra.NoArgs + + cmd.Flags().StringSliceVar(&cfg.Trust.Clean.States, "cert-states", []string{"expired"}, "Cert states to clean.") + cmd.Flags().StringVarP(&cfg.Trust.Org, "organization", "o", "", "Organization to trust.") + cmd.Flags().BoolVar(&cfg.Trust.NoSudo, "no-sudo", false, "Disable sudo prompts.") + cmd.Flags().StringVarP(&cfg.Trust.Realm, "realm", "r", "", "Realm to trust.") + cmd.Flags().StringSliceVar(&cfg.Trust.Stores, "trust-stores", []string{"homebrew", "nss", "system"}, "Trust stores to update.") + + cmd.Hidden = true +}) + type Clean struct { Anc *api.Session OrgSlug, RealmSlug string diff --git a/trust/clean_test.go b/trust/clean_test.go new file mode 100644 index 0000000..5ad5789 --- /dev/null +++ b/trust/clean_test.go @@ -0,0 +1,103 @@ +package trust + +import ( + "context" + "testing" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/cmdtest" + "github.com/anchordotdev/cli/ui/uitest" + "github.com/stretchr/testify/require" +) + +func TestCmdTrustClean(t *testing.T) { + cmd := CmdTrustClean + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "trust", "clean", "--help") + }) + + t.Run("--cert-states all", func(t *testing.T) { + t.Skip() + + t.Cleanup(func() { + cfg.Trust.Clean.States = []string{"expired"} + }) + + cmdtest.TestExecute(t, cmd, "trust", "clean", "--cert-states", "all") + + require.Equal(t, []string{"all"}, cfg.Trust.Clean.States) + }) + + t.Run("--organization test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Org = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "clean", "--organization", "test") + + require.Equal(t, "test", cfg.Trust.Org) + }) + + t.Run("-o test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Org = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "clean", "-o", "test") + + require.Equal(t, "test", cfg.Trust.Org) + }) + + t.Run("--realm test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Realm = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "clean", "--realm", "test") + + require.Equal(t, "test", cfg.Trust.Realm) + }) + + t.Run("-r test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Realm = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "clean", "-r", "test") + + require.Equal(t, "test", cfg.Trust.Realm) + }) + +} + +func TestClean(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg := new(cli.Config) + cfg.API.URL = srv.URL + cfg.Trust.MockMode = true + cfg.Trust.NoSudo = true + cfg.Trust.Stores = []string{"mock"} + var err error + if cfg.API.Token, err = srv.GeneratePAT("anky@anchor.dev"); err != nil { + t.Fatal(err) + } + ctx = cli.ContextWithConfig(ctx, cfg) + + t.Run("basics", func(t *testing.T) { + if srv.IsProxy() { + t.Skip("lcl clean unsupported in proxy mode") + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + cmd := Clean{} + + uitest.TestTUIOutput(ctx, t, cmd.UI()) + }) +} diff --git a/trust/testdata/TestClean/basics.golden b/trust/testdata/TestClean/basics.golden new file mode 100644 index 0000000..e7df422 --- /dev/null +++ b/trust/testdata/TestClean/basics.golden @@ -0,0 +1,17 @@ +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: probing credentials locally…* +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: testing credentials remotely…* +─── TrustCleanHeader ─────────────────────────────────────────────────────────── +# Clean CA Certificates from Local Trust Store(s) `anchor trust clean` +─── TrustCleanHint ───────────────────────────────────────────────────────────── +# Clean CA Certificates from Local Trust Store(s) `anchor trust clean` +| Removing CA certificates from the mock store(s). +─── TrustCleanAudit ──────────────────────────────────────────────────────────── +# Clean CA Certificates from Local Trust Store(s) `anchor trust clean` +| Removing CA certificates from the mock store(s). + * Auditing local CA certificates…* +─── TrustCleanAudit ──────────────────────────────────────────────────────────── +# Clean CA Certificates from Local Trust Store(s) `anchor trust clean` +| Removing CA certificates from the mock store(s). + - Audited local CA certificates: need to remove 0 certificates. diff --git a/trust/testdata/TestCmdTrust/--help.golden b/trust/testdata/TestCmdTrust/--help.golden new file mode 100644 index 0000000..bcde93e --- /dev/null +++ b/trust/testdata/TestCmdTrust/--help.golden @@ -0,0 +1,22 @@ +Install the AnchorCA certificates of a target organization, realm, or CA into +your local system's trust store. The default target is the localhost realm of +your personal organization. + +After installation of the AnchorCA certificates, Leaf certificates under the +AnchorCA certificates will be trusted by browsers and programs on your system. + +Usage: + anchor trust [flags] + anchor trust [command] + +Available Commands: + audit Audit CA Certificates in your Local Trust Store(s) + +Flags: + -h, --help help for trust + --no-sudo Disable sudo prompts. + -o, --organization string Organization to trust. + -r, --realm string Realm to trust. + --trust-stores strings Trust stores to update. (default [homebrew,nss,system]) + +Use "anchor trust [command] --help" for more information about a command. diff --git a/trust/testdata/TestCmdTrustAudit/--help.golden b/trust/testdata/TestCmdTrustAudit/--help.golden new file mode 100644 index 0000000..7beb2e4 --- /dev/null +++ b/trust/testdata/TestCmdTrustAudit/--help.golden @@ -0,0 +1,19 @@ +Perform an audit of the local trust store(s) and report any expected, missing, +or extra CA certificates per store. A set of expected CAs is fetched for the +target org and (optional) realm. The default stores to audit are system, nss, +and homebrew. + +CA certificate states: + + * VALID: an expected CA certificate is present in every trust store. + * MISSING: an expected CA certificate is missing in one or more stores. + * EXTRA: an unexpected CA certificate is present in one or more stores. + +Usage: + anchor trust audit [flags] + +Flags: + -h, --help help for audit + -o, --organization string Organization to trust. + -r, --realm string Realm to trust. + --trust-stores strings Trust stores to update. (default [homebrew,nss,system]) diff --git a/trust/testdata/TestCmdTrustClean/--help.golden b/trust/testdata/TestCmdTrustClean/--help.golden new file mode 100644 index 0000000..ebbc30e --- /dev/null +++ b/trust/testdata/TestCmdTrustClean/--help.golden @@ -0,0 +1,12 @@ +Clean CA Certificates from your Local Trust Store(s) + +Usage: + anchor trust clean [flags] + +Flags: + --cert-states strings Cert states to clean. (default [expired]) + -h, --help help for clean + --no-sudo Disable sudo prompts. + -o, --organization string Organization to trust. + -r, --realm string Realm to trust. + --trust-stores strings Trust stores to update. (default [homebrew,nss,system]) diff --git a/trust/trust.go b/trust/trust.go index f970648..60bdf9e 100644 --- a/trust/trust.go +++ b/trust/trust.go @@ -18,8 +18,20 @@ import ( "github.com/anchordotdev/cli/trust/models" "github.com/anchordotdev/cli/truststore" "github.com/anchordotdev/cli/ui" + "github.com/spf13/cobra" ) +var CmdTrust = cli.NewCmd[Command](cli.CmdRoot, "trust", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + + cmd.Args = cobra.NoArgs + + cmd.Flags().StringVarP(&cfg.Trust.Org, "organization", "o", "", "Organization to trust.") + cmd.Flags().BoolVar(&cfg.Trust.NoSudo, "no-sudo", false, "Disable sudo prompts.") + cmd.Flags().StringVarP(&cfg.Trust.Realm, "realm", "r", "", "Realm to trust.") + cmd.Flags().StringSliceVar(&cfg.Trust.Stores, "trust-stores", []string{"homebrew", "nss", "system"}, "Trust stores to update.") +}) + type Command struct { Anc *api.Session OrgSlug, RealmSlug string diff --git a/trust/trust_test.go b/trust/trust_test.go index f50f2ed..ac17b79 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -8,7 +8,9 @@ import ( "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api/apitest" + "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/ui/uitest" + "github.com/stretchr/testify/require" ) var srv = &apitest.Server{ @@ -28,6 +30,76 @@ func TestMain(m *testing.M) { srv.Close() } +func TestCmdTrust(t *testing.T) { + cmd := CmdTrust + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "trust", "--help") + }) + + t.Run("--no-sudo", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Org = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "--no-sudo") + + require.Equal(t, true, cfg.Trust.NoSudo) + }) + + t.Run("--organization test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Org = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "--organization", "test") + + require.Equal(t, "test", cfg.Trust.Org) + }) + + t.Run("-o test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Org = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "-o", "test") + + require.Equal(t, "test", cfg.Trust.Org) + }) + + t.Run("--realm test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Realm = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "--realm", "test") + + require.Equal(t, "test", cfg.Trust.Realm) + }) + + t.Run("-r test", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Realm = "" + }) + + cmdtest.TestExecute(t, cmd, "trust", "-r", "test") + + require.Equal(t, "test", cfg.Trust.Realm) + }) + + t.Run("--trust-stores nss,system", func(t *testing.T) { + t.Cleanup(func() { + cfg.Trust.Stores = []string{"homebrew", "nss", "system"} + }) + + cmdtest.TestExecute(t, cmd, "trust", "--trust-stores", "nss,system") + + require.Equal(t, []string{"nss", "system"}, cfg.Trust.Stores) + }) +} + func TestTrust(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/version/command.go b/version/command.go index 7802d80..983afad 100644 --- a/version/command.go +++ b/version/command.go @@ -5,10 +5,15 @@ import ( "fmt" "github.com/muesli/termenv" + "github.com/spf13/cobra" "github.com/anchordotdev/cli" ) +var CmdVersion = cli.NewCmd[Command](cli.CmdRoot, "version", func(cmd *cobra.Command) { + cmd.Args = cobra.NoArgs +}) + type Command struct{} func (c Command) UI() cli.UI { diff --git a/version/command_test.go b/version/command_test.go index badcfc2..02d0bd0 100644 --- a/version/command_test.go +++ b/version/command_test.go @@ -2,16 +2,23 @@ package version import ( "context" - "flag" "runtime" "testing" + "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api/apitest" + "github.com/anchordotdev/cli/cmdtest" ) -var ( - _ = flag.Bool("update", false, "ignored") -) +func TestCmdVersion(t *testing.T) { + cmd := CmdVersion + cfg := cli.ConfigFromCmd(cmd) + cfg.Test.SkipRunE = true + + t.Run("--help", func(t *testing.T) { + cmdtest.TestOutput(t, cmd, "version", "--help") + }) +} func TestCommand(t *testing.T) { if runtime.GOOS == "windows" { diff --git a/version/testdata/TestCmdVersion/--help.golden b/version/testdata/TestCmdVersion/--help.golden new file mode 100644 index 0000000..60cccd0 --- /dev/null +++ b/version/testdata/TestCmdVersion/--help.golden @@ -0,0 +1,7 @@ +Show version info + +Usage: + anchor version [flags] + +Flags: + -h, --help help for version