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 0000000..30241ed Binary files /dev/null and b/cmd/anchor/windows/als2.ico differ 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