diff --git a/CLAUDE.md b/CLAUDE.md index 09a86cc..d7c978e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,7 +77,7 @@ Configuration lives in `.core/build.yaml` (targets, ldflags) and `.core/release. ## Coding Standards - **UK English**: colour, organisation, centre -- **Tests**: testify assert/require, `_Good`/`_Bad`/`_Ugly` naming convention +- **Tests**: standard `testing` checks only; no testify. Use `_Good`/`_Bad`/`_Ugly` naming convention - **Conventional commits**: `feat(ansible):`, `fix(infra):`, `refactor(build):` - **Co-Author**: `Co-Authored-By: Virgil ` - **Licence**: EUPL-1.2 diff --git a/cmd/deploy/ax7_test.go b/cmd/deploy/ax7_test.go new file mode 100644 index 0000000..8eb2b2d --- /dev/null +++ b/cmd/deploy/ax7_test.go @@ -0,0 +1,39 @@ +package deploy + +import ( + core "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func TestAX7_AddDeployCommands_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddDeployCommands(root) + commands := root.Commands() + + core.AssertLen(t, commands, 1) + core.AssertEqual(t, "deploy", commands[0].Use) +} + +func TestAX7_AddDeployCommands_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddDeployCommands(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddDeployCommands_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddDeployCommands(root) + + foundExisting := false + foundDeploy := false + for _, cmd := range root.Commands() { + foundExisting = foundExisting || cmd.Use == "existing" + foundDeploy = foundDeploy || cmd.Use == "deploy" + } + core.AssertLen(t, root.Commands(), 2) + core.AssertTrue(t, foundExisting) + core.AssertTrue(t, foundDeploy) +} diff --git a/cmd/dev/ax7_bundle_test.go b/cmd/dev/ax7_bundle_test.go new file mode 100644 index 0000000..8cf6ebf --- /dev/null +++ b/cmd/dev/ax7_bundle_test.go @@ -0,0 +1,80 @@ +package dev + +import core "dappco.re/go" + +func TestAX7_NewWorkBundle_Good(t *core.T) { + bundle, err := NewWorkBundle(WorkBundleOptions{}) + core.AssertNoError(t, err) + + core.AssertNotNil(t, bundle) + core.AssertNotNil(t, bundle.Core) +} + +func TestAX7_NewWorkBundle_Bad(t *core.T) { + bundle, err := NewWorkBundle(WorkBundleOptions{RegistryPath: "\x00"}) + core.AssertNoError(t, err) + + core.AssertNotNil(t, bundle) + core.AssertNotNil(t, bundle.Core) +} + +func TestAX7_NewWorkBundle_Ugly(t *core.T) { + first, err := NewWorkBundle(WorkBundleOptions{}) + core.RequireNoError(t, err) + second, err := NewWorkBundle(WorkBundleOptions{}) + + core.AssertNoError(t, err) + core.AssertFalse(t, first.Core == second.Core) +} + +func TestAX7_WorkBundle_Start_Good(t *core.T) { + bundle, err := NewWorkBundle(WorkBundleOptions{}) + core.RequireNoError(t, err) + + core.AssertNoError(t, bundle.Start(core.Background())) + core.AssertNoError(t, bundle.Stop(core.Background())) +} + +func TestAX7_WorkBundle_Start_Bad(t *core.T) { + var bundle *WorkBundle + core.AssertPanics(t, func() { + _ = bundle.Start(core.Background()) + }) + core.AssertNil(t, bundle) +} + +func TestAX7_WorkBundle_Start_Ugly(t *core.T) { + bundle, err := NewWorkBundle(WorkBundleOptions{}) + core.RequireNoError(t, err) + err = bundle.Start(core.Background()) + + core.AssertNoError(t, err) + core.AssertNoError(t, bundle.Start(core.Background())) + core.AssertNoError(t, bundle.Stop(core.Background())) +} + +func TestAX7_WorkBundle_Stop_Good(t *core.T) { + bundle, err := NewWorkBundle(WorkBundleOptions{}) + core.RequireNoError(t, err) + core.RequireNoError(t, bundle.Start(core.Background())) + + err = bundle.Stop(core.Background()) + core.AssertNoError(t, err) +} + +func TestAX7_WorkBundle_Stop_Bad(t *core.T) { + var bundle *WorkBundle + core.AssertPanics(t, func() { + _ = bundle.Stop(core.Background()) + }) + core.AssertNil(t, bundle) +} + +func TestAX7_WorkBundle_Stop_Ugly(t *core.T) { + bundle, err := NewWorkBundle(WorkBundleOptions{}) + core.RequireNoError(t, err) + + err = bundle.Stop(core.Background()) + core.AssertNoError(t, err) + core.AssertNotNil(t, bundle.Core) +} diff --git a/cmd/dev/ax7_commands_test.go b/cmd/dev/ax7_commands_test.go new file mode 100644 index 0000000..d96accd --- /dev/null +++ b/cmd/dev/ax7_commands_test.go @@ -0,0 +1,249 @@ +package dev + +import ( + core "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func ax7Command(root *cli.Command, use string) *cli.Command { + for _, cmd := range root.Commands() { + if cmd.Use == use || core.HasPrefix(cmd.Use, use+" ") { + return cmd + } + } + return nil +} + +func TestAX7_AddDevCommands_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddDevCommands(root) + devCmd := ax7Command(root, "dev") + + core.AssertNotNil(t, devCmd) + core.AssertGreaterOrEqual(t, len(devCmd.Commands()), 10) +} + +func TestAX7_AddDevCommands_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddDevCommands(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddDevCommands_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddDevCommands(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "dev")) +} + +func TestAX7_AddApplyCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddApplyCommand(root) + cmd := ax7Command(root, "apply") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("command")) +} + +func TestAX7_AddApplyCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddApplyCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddApplyCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + AddApplyCommand(root) + AddApplyCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "apply")) +} + +func TestAX7_AddFileSyncCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddFileSyncCommand(root) + cmd := ax7Command(root, "sync") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("to")) +} + +func TestAX7_AddFileSyncCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddFileSyncCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddFileSyncCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + AddFileSyncCommand(root) + AddFileSyncCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "sync")) +} + +func TestAX7_AddPushCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddPushCommand(root) + cmd := ax7Command(root, "push") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("force")) +} + +func TestAX7_AddPushCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddPushCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddPushCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddPushCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "push")) +} + +func TestAX7_AddWorkCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddWorkCommand(root) + cmd := ax7Command(root, "work") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("status")) +} + +func TestAX7_AddWorkCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddWorkCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddWorkCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + AddWorkCommand(root) + AddWorkCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "work")) +} + +func TestAX7_AddCommitCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddCommitCommand(root) + cmd := ax7Command(root, "commit") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("all")) +} + +func TestAX7_AddCommitCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddCommitCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddCommitCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddCommitCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "commit")) +} + +func TestAX7_AddHealthCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddHealthCommand(root) + cmd := ax7Command(root, "health") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("verbose")) +} + +func TestAX7_AddHealthCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddHealthCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddHealthCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + AddHealthCommand(root) + AddHealthCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "health")) +} + +func TestAX7_AddTagCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddTagCommand(root) + cmd := ax7Command(root, "tag") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("dry-run")) +} + +func TestAX7_AddTagCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddTagCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddTagCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddTagCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "tag")) +} + +func TestAX7_AddPullCommand_Good(t *core.T) { + root := &cli.Command{Use: "root"} + AddPullCommand(root) + cmd := ax7Command(root, "pull") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("all")) +} + +func TestAX7_AddPullCommand_Bad(t *core.T) { + var root *cli.Command + core.AssertPanics(t, func() { + AddPullCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddPullCommand_Ugly(t *core.T) { + root := &cli.Command{Use: "root"} + AddPullCommand(root) + AddPullCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7Command(root, "pull")) +} diff --git a/cmd/dev/cmd_api.go b/cmd/dev/cmd_api.go index 1d2dcad..c36ed9b 100644 --- a/cmd/dev/cmd_api.go +++ b/cmd/dev/cmd_api.go @@ -1,8 +1,8 @@ package dev import ( - "dappco.re/go/i18n" "dappco.re/go/cli/pkg/cli" + "dappco.re/go/i18n" ) // addAPICommands adds the 'api' command and its subcommands to the given parent command. diff --git a/cmd/dev/cmd_api_testgen.go b/cmd/dev/cmd_api_testgen.go index 0e63168..5fa0b81 100644 --- a/cmd/dev/cmd_api_testgen.go +++ b/cmd/dev/cmd_api_testgen.go @@ -5,9 +5,9 @@ import ( "path/filepath" "text/template" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" coreio "dappco.re/go/io" - "dappco.re/go/cli/pkg/cli" ) func addTestGenCommand(parent *cli.Command) { diff --git a/cmd/dev/cmd_api_testgen_test.go b/cmd/dev/cmd_api_testgen_test.go index f2395c2..39eca39 100644 --- a/cmd/dev/cmd_api_testgen_test.go +++ b/cmd/dev/cmd_api_testgen_test.go @@ -1,8 +1,8 @@ package dev import ( - "os" "path/filepath" + "slices" "strings" "testing" @@ -11,17 +11,13 @@ import ( func TestRunTestGen_Good(t *testing.T) { tmpDir := t.TempDir() - - originalWD, err := os.Getwd() - mustNoError(t, err) - t.Cleanup(func() { - _ = os.Chdir(originalWD) - }) - mustNoError(t, os.Chdir(tmpDir)) + t.Setenv("CORE_WORKING_DIRECTORY", tmpDir) serviceDir := filepath.Join(tmpDir, "pkg", "demo") - mustNoError(t, io.Local.EnsureDir(serviceDir)) - mustNoError(t, io.Local.Write(filepath.Join(serviceDir, "demo.go"), `package demo + if err := io.Local.EnsureDir(serviceDir); err != nil { + t.Fatalf("create service dir: %v", err) + } + if err := io.Local.Write(filepath.Join(serviceDir, "demo.go"), `package demo type Example struct{} @@ -30,40 +26,58 @@ const Answer = 42 var Value = Example{} func Run() {} -`)) - mustNoError(t, io.Local.Write(filepath.Join(serviceDir, "extra.go"), `package demo +`); err != nil { + t.Fatalf("write demo.go: %v", err) + } + if err := io.Local.Write(filepath.Join(serviceDir, "extra.go"), `package demo type Another struct{} func Extra() {} -`)) - mustNoError(t, io.Local.Write(filepath.Join(serviceDir, "demo_test.go"), `package demo +`); err != nil { + t.Fatalf("write extra.go: %v", err) + } + if err := io.Local.Write(filepath.Join(serviceDir, "demo_test.go"), `package demo func Ignored() {} -`)) +`); err != nil { + t.Fatalf("write demo_test.go: %v", err) + } - mustNoError(t, runTestGen()) + if err := runTestGen(); err != nil { + t.Fatalf("run test generator: %v", err) + } generatedPath := filepath.Join(tmpDir, "demo", "demo_test.go") content, err := io.Local.Read(generatedPath) - mustNoError(t, err) - - mustContains(t, content, `// Code generated by "core dev api test-gen"; DO NOT EDIT.`) - mustContains(t, content, `package demo`) - mustContains(t, content, `impl "dappco.re/go/cli/demo"`) - mustContains(t, content, `type _ = impl.Example`) - mustContains(t, content, `type _ = impl.Another`) - mustContains(t, content, `const _ = impl.Answer`) - mustContains(t, content, `var _ = impl.Value`) - mustContains(t, content, `var _ = impl.Run`) - mustContains(t, content, `var _ = impl.Extra`) - mustNotContains(t, content, `Ignored`) + if err != nil { + t.Fatalf("read generated file: %v", err) + } + + for _, want := range []string{ + `// Code generated by "core dev api test-gen"; DO NOT EDIT.`, + `package demo`, + `impl "dappco.re/go/cli/demo"`, + `type _ = impl.Example`, + `type _ = impl.Another`, + `const _ = impl.Answer`, + `var _ = impl.Value`, + `var _ = impl.Run`, + `var _ = impl.Extra`, + } { + if !strings.Contains(content, want) { + t.Fatalf("generated content missing %q", want) + } + } + if strings.Contains(content, `Ignored`) { + t.Fatal("generated content includes ignored symbol") + } } func TestGeneratePublicAPITestFile_Good(t *testing.T) { tmpDir := t.TempDir() - mustNoError(t, generatePublicAPITestFile( + if err := generatePublicAPITestFile( filepath.Join(tmpDir, "demo"), filepath.Join(tmpDir, "demo", "demo_test.go"), "demo", @@ -71,44 +85,63 @@ func TestGeneratePublicAPITestFile_Good(t *testing.T) { {Name: "Example", Kind: "type"}, {Name: "Answer", Kind: "const"}, }, - )) + ); err != nil { + t.Fatalf("generate public API test file: %v", err) + } content, err := io.Local.Read(filepath.Join(tmpDir, "demo", "demo_test.go")) - mustNoError(t, err) + if err != nil { + t.Fatalf("read generated public API test file: %v", err) + } - mustTrue(t, strings.Contains(content, `type _ = impl.Example`)) - mustTrue(t, strings.Contains(content, `const _ = impl.Answer`)) + for _, want := range []string{`type _ = impl.Example`, `const _ = impl.Answer`} { + if !strings.Contains(content, want) { + t.Fatalf("generated content missing %q", want) + } + } } func TestGetExportedSymbols_MultiFile_Good(t *testing.T) { tmpDir := t.TempDir() serviceDir := filepath.Join(tmpDir, "demo") - mustNoError(t, io.Local.EnsureDir(serviceDir)) - mustNoError(t, io.Local.Write(filepath.Join(serviceDir, "demo.go"), `package demo + if err := io.Local.EnsureDir(serviceDir); err != nil { + t.Fatalf("create service dir: %v", err) + } + if err := io.Local.Write(filepath.Join(serviceDir, "demo.go"), `package demo type Example struct{} const Answer = 42 -`)) - mustNoError(t, io.Local.Write(filepath.Join(serviceDir, "extra.go"), `package demo +`); err != nil { + t.Fatalf("write demo.go: %v", err) + } + if err := io.Local.Write(filepath.Join(serviceDir, "extra.go"), `package demo var Value = Example{} func Run() {} -`)) - mustNoError(t, io.Local.Write(filepath.Join(serviceDir, "demo_test.go"), `package demo +`); err != nil { + t.Fatalf("write extra.go: %v", err) + } + if err := io.Local.Write(filepath.Join(serviceDir, "demo_test.go"), `package demo type Ignored struct{} -`)) +`); err != nil { + t.Fatalf("write demo_test.go: %v", err) + } symbols, err := getExportedSymbols(serviceDir) - mustNoError(t, err) + if err != nil { + t.Fatalf("get exported symbols: %v", err) + } want := []symbolInfo{ {Name: "Answer", Kind: "const"}, {Name: "Example", Kind: "type"}, {Name: "Run", Kind: "func"}, {Name: "Value", Kind: "var"}, } - mustDeepEqual(t, want, symbols) + if !slices.Equal(symbols, want) { + t.Fatalf("symbols = %v, want %v", symbols, want) + } } diff --git a/cmd/dev/cmd_apply.go b/cmd/dev/cmd_apply.go index aa58abd..0f6f4b1 100644 --- a/cmd/dev/cmd_apply.go +++ b/cmd/dev/cmd_apply.go @@ -14,12 +14,12 @@ import ( "path/filepath" "sort" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" "dappco.re/go/io" core "dappco.re/go/log" "dappco.re/go/scm/git" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" ) // Apply command flags diff --git a/cmd/dev/cmd_apply_test.go b/cmd/dev/cmd_apply_test.go index d2c6d6c..b406bdd 100644 --- a/cmd/dev/cmd_apply_test.go +++ b/cmd/dev/cmd_apply_test.go @@ -17,21 +17,34 @@ func TestFilterTargetRepos_Good(t *testing.T) { t.Run("exact names", func(t *testing.T) { matched := filterTargetRepos(registry, "core-api,docs-site") - mustLen(t, matched, 2) - mustEqual(t, "core-api", matched[0].Name) - mustEqual(t, "docs-site", matched[1].Name) + if len(matched) != 2 { + t.Fatalf("matched length = %d, want 2", len(matched)) + } + if matched[0].Name != "core-api" { + t.Fatalf("matched[0].Name = %q, want %q", matched[0].Name, "core-api") + } + if matched[1].Name != "docs-site" { + t.Fatalf("matched[1].Name = %q, want %q", matched[1].Name, "docs-site") + } }) t.Run("glob patterns", func(t *testing.T) { matched := filterTargetRepos(registry, "core-*,sites/*") - mustLen(t, matched, 3) - mustEqual(t, "core-api", matched[0].Name) - mustEqual(t, "core-web", matched[1].Name) - mustEqual(t, "docs-site", matched[2].Name) + if len(matched) != 3 { + t.Fatalf("matched length = %d, want 3", len(matched)) + } + wantNames := []string{"core-api", "core-web", "docs-site"} + for i, want := range wantNames { + if matched[i].Name != want { + t.Fatalf("matched[%d].Name = %q, want %q", i, matched[i].Name, want) + } + } }) t.Run("all repos when empty", func(t *testing.T) { matched := filterTargetRepos(registry, "") - mustLen(t, matched, 3) + if len(matched) != 3 { + t.Fatalf("matched length = %d, want 3", len(matched)) + } }) } diff --git a/cmd/dev/cmd_bundles.go b/cmd/dev/cmd_bundles.go index 106b52b..816ecc8 100644 --- a/cmd/dev/cmd_bundles.go +++ b/cmd/dev/cmd_bundles.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "dappco.re/go/core" + "dappco.re/go" ) // WorkBundle contains the Core instance for dev work operations. @@ -31,7 +31,7 @@ func NewWorkBundle(opts WorkBundleOptions) (*WorkBundle, error) { c.Service("dev", core.Service{ OnStart: func() core.Result { c.RegisterAction(svc.handleAction) - return core.Result{OK: true} + return core.Ok(nil) }, }) diff --git a/cmd/dev/cmd_commit.go b/cmd/dev/cmd_commit.go index 5b93d60..a98b47f 100644 --- a/cmd/dev/cmd_commit.go +++ b/cmd/dev/cmd_commit.go @@ -6,9 +6,9 @@ import ( "path/filepath" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/scm/git" "dappco.re/go/i18n" coreio "dappco.re/go/io" + "dappco.re/go/scm/git" ) // Commit command flags diff --git a/cmd/dev/cmd_dev.go b/cmd/dev/cmd_dev.go index a177c8a..6cf8341 100644 --- a/cmd/dev/cmd_dev.go +++ b/cmd/dev/cmd_dev.go @@ -34,8 +34,8 @@ package dev import ( - "dappco.re/go/i18n" "dappco.re/go/cli/pkg/cli" + "dappco.re/go/i18n" _ "dappco.re/go/devops/locales" ) diff --git a/cmd/dev/cmd_file_sync.go b/cmd/dev/cmd_file_sync.go index abdb3f2..34abe3e 100644 --- a/cmd/dev/cmd_file_sync.go +++ b/cmd/dev/cmd_file_sync.go @@ -14,12 +14,12 @@ import ( "path/filepath" "strings" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" coreio "dappco.re/go/io" "dappco.re/go/log" "dappco.re/go/scm/git" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" ) // File sync command flags @@ -51,7 +51,9 @@ func AddFileSyncCommand(parent *cli.Command) { syncCmd.Flags().BoolVar(&fileSyncPush, "push", false, i18n.T("cmd.dev.file_sync.flag.push")) syncCmd.Flags().BoolVarP(&fileSyncYes, "yes", "y", false, i18n.T("cmd.dev.file_sync.flag.yes")) - _ = syncCmd.MarkFlagRequired("to") + if err := syncCmd.MarkFlagRequired("to"); err != nil { + panic(err) + } parent.AddCommand(syncCmd) } diff --git a/cmd/dev/cmd_file_sync_test.go b/cmd/dev/cmd_file_sync_test.go index 7ac5fef..63646dd 100644 --- a/cmd/dev/cmd_file_sync_test.go +++ b/cmd/dev/cmd_file_sync_test.go @@ -1,6 +1,7 @@ package dev import ( + "slices" "testing" "dappco.re/go/cli/pkg/cli" @@ -12,28 +13,63 @@ func TestAddFileSyncCommand_Good(t *testing.T) { AddDevCommands(root) syncCmd, _, err := root.Find([]string{"dev", "sync"}) - mustNoError(t, err) - mustNotNil(t, syncCmd) + if err != nil { + t.Fatalf("find sync command: %v", err) + } + if syncCmd == nil { + t.Fatal("expected sync command") + } yesFlag := syncCmd.Flags().Lookup("yes") - mustNotNil(t, yesFlag) - mustEqual(t, "y", yesFlag.Shorthand) + if yesFlag == nil { + t.Fatal("expected yes flag") + } + if yesFlag.Shorthand != "y" { + t.Fatalf("yes shorthand = %q, want %q", yesFlag.Shorthand, "y") + } - mustNotNil(t, syncCmd.Flags().Lookup("dry-run")) - mustNotNil(t, syncCmd.Flags().Lookup("push")) + if syncCmd.Flags().Lookup("dry-run") == nil { + t.Fatal("expected dry-run flag") + } + if syncCmd.Flags().Lookup("push") == nil { + t.Fatal("expected push flag") + } } func TestSplitPatterns_Good(t *testing.T) { patterns := splitPatterns("packages/core-*, apps/* ,services/*,") want := []string{"packages/core-*", "apps/*", "services/*"} - mustDeepEqual(t, want, patterns) + if !slices.Equal(patterns, want) { + t.Fatalf("patterns = %v, want %v", patterns, want) + } } func TestMatchGlob_Good(t *testing.T) { - mustTrue(t, matchGlob("packages/core-xyz", "packages/core-*")) - mustTrue(t, matchGlob("packages/core-xyz", "*/core-*")) - mustTrue(t, matchGlob("a-b", "a?b")) - mustTrue(t, matchGlob("foo", "foo")) - mustFalse(t, matchGlob("core-other", "packages/*")) - mustFalse(t, matchGlob("abc", "[]")) + trueCases := []struct { + name string + pattern string + }{ + {name: "packages/core-xyz", pattern: "packages/core-*"}, + {name: "packages/core-xyz", pattern: "*/core-*"}, + {name: "a-b", pattern: "a?b"}, + {name: "foo", pattern: "foo"}, + } + for _, tc := range trueCases { + if !matchGlob(tc.name, tc.pattern) { + t.Fatalf("matchGlob(%q, %q) = false, want true", tc.name, tc.pattern) + } + } + + falseCases := []struct { + name string + pattern string + }{ + {name: "core-other", pattern: "packages/*"}, + {name: "abc", pattern: "[]"}, + } + for _, tc := range falseCases { + if matchGlob(tc.name, tc.pattern) { + t.Fatalf("matchGlob(%q, %q) = true, want false", tc.name, tc.pattern) + } + } } diff --git a/cmd/dev/cmd_health.go b/cmd/dev/cmd_health.go index aa9c8bb..d4ac02f 100644 --- a/cmd/dev/cmd_health.go +++ b/cmd/dev/cmd_health.go @@ -8,8 +8,8 @@ import ( "strings" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/scm/git" "dappco.re/go/i18n" + "dappco.re/go/scm/git" ) // Health command flags diff --git a/cmd/dev/cmd_pull.go b/cmd/dev/cmd_pull.go index b3aecc7..1256b6b 100644 --- a/cmd/dev/cmd_pull.go +++ b/cmd/dev/cmd_pull.go @@ -5,8 +5,8 @@ import ( "os/exec" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/scm/git" "dappco.re/go/i18n" + "dappco.re/go/scm/git" ) // Pull command flags diff --git a/cmd/dev/cmd_push.go b/cmd/dev/cmd_push.go index e3a715a..76a397d 100644 --- a/cmd/dev/cmd_push.go +++ b/cmd/dev/cmd_push.go @@ -6,8 +6,8 @@ import ( "path/filepath" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/scm/git" "dappco.re/go/i18n" + "dappco.re/go/scm/git" ) // Push command flags diff --git a/cmd/dev/cmd_vm.go b/cmd/dev/cmd_vm.go index 2e378a4..fa3f35e 100644 --- a/cmd/dev/cmd_vm.go +++ b/cmd/dev/cmd_vm.go @@ -5,11 +5,11 @@ import ( "os" "time" + "dappco.re/go/cli/pkg/cli" + "dappco.re/go/container/devenv" "dappco.re/go/i18n" "dappco.re/go/io" log "dappco.re/go/log" - "dappco.re/go/cli/pkg/cli" - "dappco.re/go/container/devenv" ) // addVMCommands adds the dev environment VM commands to the dev parent command. @@ -482,10 +482,15 @@ func runVMUpdate(apply bool) error { } // Stop if running - running, _ := d.IsRunning(ctx) + running, err := d.IsRunning(ctx) + if err != nil { + return cli.Wrap(err, "failed to check VM state") + } if running { cli.Text(i18n.T("cmd.dev.vm.stopping_current")) - _ = d.Stop(ctx) + if err := d.Stop(ctx); err != nil { + return cli.Wrap(err, "failed to stop current VM") + } } cli.Text(i18n.T("cmd.dev.vm.downloading_update")) diff --git a/cmd/dev/cmd_vm_test.go b/cmd/dev/cmd_vm_test.go index 2281bf0..16a2ad5 100644 --- a/cmd/dev/cmd_vm_test.go +++ b/cmd/dev/cmd_vm_test.go @@ -1,6 +1,7 @@ package dev import ( + "slices" "testing" "dappco.re/go/cli/pkg/cli" @@ -12,13 +13,27 @@ func TestAddVMStatusCommand_Good(t *testing.T) { AddDevCommands(root) statusCmd, _, err := root.Find([]string{"dev", "status"}) - mustNoError(t, err) - mustNotNil(t, statusCmd) - mustEqual(t, "status", statusCmd.Use) - mustContainsString(t, statusCmd.Aliases, "vm-status") + if err != nil { + t.Fatalf("find status command: %v", err) + } + if statusCmd == nil { + t.Fatal("expected status command") + } + if statusCmd.Use != "status" { + t.Fatalf("status command use = %q, want %q", statusCmd.Use, "status") + } + if !slices.Contains(statusCmd.Aliases, "vm-status") { + t.Fatalf("status aliases = %v, want vm-status", statusCmd.Aliases) + } aliasCmd, _, err := root.Find([]string{"dev", "vm-status"}) - mustNoError(t, err) - mustNotNil(t, aliasCmd) - mustTrue(t, statusCmd == aliasCmd) + if err != nil { + t.Fatalf("find vm-status alias: %v", err) + } + if aliasCmd == nil { + t.Fatal("expected vm-status alias command") + } + if statusCmd != aliasCmd { + t.Fatal("expected vm-status alias to resolve to status command") + } } diff --git a/cmd/dev/cmd_work.go b/cmd/dev/cmd_work.go index 6d0759f..adfdd6e 100644 --- a/cmd/dev/cmd_work.go +++ b/cmd/dev/cmd_work.go @@ -51,7 +51,11 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { if err := bundle.Start(ctx); err != nil { return err } - defer func() { _ = bundle.Stop(ctx) }() + defer func() { + if err := bundle.Stop(ctx); err != nil { + cli.Print(" %s %s\n", errorStyle.Render("x"), err) + } + }() // Load registry and get paths reg, _, err := loadRegistryWithConfig(registryPath) @@ -286,4 +290,3 @@ func printStatusTable(statuses []git.RepoStatus) { ) } } - diff --git a/cmd/dev/cmd_workflow.go b/cmd/dev/cmd_workflow.go index 7bffdaa..b91153b 100644 --- a/cmd/dev/cmd_workflow.go +++ b/cmd/dev/cmd_workflow.go @@ -7,10 +7,10 @@ import ( "slices" "strings" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" "dappco.re/go/io" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" ) // Workflow command flags diff --git a/cmd/dev/cmd_workflow_test.go b/cmd/dev/cmd_workflow_test.go index 82d6080..1700018 100644 --- a/cmd/dev/cmd_workflow_test.go +++ b/cmd/dev/cmd_workflow_test.go @@ -13,18 +13,26 @@ func TestFindWorkflows_Good(t *testing.T) { // Create a temp directory with workflow files tmpDir := t.TempDir() workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - mustNoError(t, io.Local.EnsureDir(workflowsDir)) + if err := io.Local.EnsureDir(workflowsDir); err != nil { + t.Fatalf("create workflows dir: %v", err) + } // Create some workflow files for _, name := range []string{"qa.yml", "tests.yml", "codeql.yaml"} { - mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, name), "name: Test")) + if err := io.Local.Write(filepath.Join(workflowsDir, name), "name: Test"); err != nil { + t.Fatalf("write workflow %s: %v", name, err) + } } // Create a non-workflow file (should be ignored) - mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, "readme.md"), "# Workflows")) + if err := io.Local.Write(filepath.Join(workflowsDir, "readme.md"), "# Workflows"); err != nil { + t.Fatalf("write readme: %v", err) + } workflows := findWorkflows(tmpDir) - mustLen(t, workflows, 3) + if len(workflows) != 3 { + t.Fatalf("workflows length = %d, want 3", len(workflows)) + } // Check that all expected workflows are found found := make(map[string]bool) @@ -33,7 +41,9 @@ func TestFindWorkflows_Good(t *testing.T) { } for _, expected := range []string{"qa.yml", "tests.yml", "codeql.yaml"} { - mustTrue(t, found[expected]) + if !found[expected] { + t.Fatalf("expected workflow %s in %v", expected, workflows) + } } } @@ -41,43 +51,61 @@ func TestFindWorkflows_NoWorkflowsDir_Bad(t *testing.T) { tmpDir := t.TempDir() workflows := findWorkflows(tmpDir) - mustLen(t, workflows, 0) + if len(workflows) != 0 { + t.Fatalf("workflows length = %d, want 0", len(workflows)) + } } func TestFindTemplateWorkflow_Good(t *testing.T) { tmpDir := t.TempDir() templatesDir := filepath.Join(tmpDir, ".github", "workflow-templates") - mustNoError(t, io.Local.EnsureDir(templatesDir)) + if err := io.Local.EnsureDir(templatesDir); err != nil { + t.Fatalf("create templates dir: %v", err) + } templateContent := "name: QA\non: [push]" - mustNoError(t, io.Local.Write(filepath.Join(templatesDir, "qa.yml"), templateContent)) + if err := io.Local.Write(filepath.Join(templatesDir, "qa.yml"), templateContent); err != nil { + t.Fatalf("write template workflow: %v", err) + } // Test finding with .yml extension result := findTemplateWorkflow(tmpDir, "qa.yml") - mustTrue(t, result != "") + if result == "" { + t.Fatal("expected template workflow for qa.yml") + } // Test finding without extension (should auto-add .yml) result = findTemplateWorkflow(tmpDir, "qa") - mustTrue(t, result != "") + if result == "" { + t.Fatal("expected template workflow for qa") + } } func TestFindTemplateWorkflow_FallbackToWorkflows_Good(t *testing.T) { tmpDir := t.TempDir() workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - mustNoError(t, io.Local.EnsureDir(workflowsDir)) + if err := io.Local.EnsureDir(workflowsDir); err != nil { + t.Fatalf("create workflows dir: %v", err) + } templateContent := "name: Tests\non: [push]" - mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, "tests.yml"), templateContent)) + if err := io.Local.Write(filepath.Join(workflowsDir, "tests.yml"), templateContent); err != nil { + t.Fatalf("write workflow: %v", err) + } result := findTemplateWorkflow(tmpDir, "tests.yml") - mustTrue(t, result != "") + if result == "" { + t.Fatal("expected fallback workflow") + } } func TestFindTemplateWorkflow_NotFound_Bad(t *testing.T) { tmpDir := t.TempDir() result := findTemplateWorkflow(tmpDir, "nonexistent.yml") - mustEqual(t, "", result) + if result != "" { + t.Fatalf("result = %q, want empty", result) + } } func TestTemplateNames_Good(t *testing.T) { @@ -89,8 +117,8 @@ func TestTemplateNames_Good(t *testing.T) { names := slices.Sorted(maps.Keys(templateSet)) - mustLen(t, names, 3) - mustEqual(t, "a.yml", names[0]) - mustEqual(t, "m.yml", names[1]) - mustEqual(t, "z.yml", names[2]) + want := []string{"a.yml", "m.yml", "z.yml"} + if !slices.Equal(names, want) { + t.Fatalf("names = %v, want %v", names, want) + } } diff --git a/cmd/dev/registry.go b/cmd/dev/registry.go index 12cb819..f49f3ad 100644 --- a/cmd/dev/registry.go +++ b/cmd/dev/registry.go @@ -5,11 +5,11 @@ import ( "path/filepath" "strings" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/devops/cmd/workspace" "dappco.re/go/i18n" "dappco.re/go/io" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" ) // loadRegistryWithConfig loads the registry and applies workspace configuration. diff --git a/cmd/dev/service.go b/cmd/dev/service.go index 5c22f79..8378bd8 100644 --- a/cmd/dev/service.go +++ b/cmd/dev/service.go @@ -5,8 +5,8 @@ import ( "os" "os/exec" + "dappco.re/go" "dappco.re/go/agent/pkg/lib" - "dappco.re/go/core" ) // ServiceOptions for configuring the dev service. @@ -20,7 +20,7 @@ type Service struct { } func (s *Service) handleAction(_ *core.Core, _ core.Message) core.Result { - return core.Result{OK: true} + return core.Ok(nil) } // doCommit shells out to claude for AI-assisted commit. diff --git a/cmd/dev/test_helpers_test.go b/cmd/dev/test_helpers_test.go deleted file mode 100644 index 2a85ade..0000000 --- a/cmd/dev/test_helpers_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package dev - -import ( - "reflect" - "strings" - "testing" -) - -func mustNoError(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func mustEqual[T comparable](t *testing.T, want, got T) { - t.Helper() - if want != got { - t.Fatalf("want %v, got %v", want, got) - } -} - -func mustDeepEqual(t *testing.T, want, got any) { - t.Helper() - if !reflect.DeepEqual(want, got) { - t.Fatalf("want %v, got %v", want, got) - } -} - -func mustContains(t *testing.T, s, sub string) { - t.Helper() - if !strings.Contains(s, sub) { - t.Fatalf("expected %q to contain %q", s, sub) - } -} - -func mustContainsString(t *testing.T, haystack []string, needle string) { - t.Helper() - for _, s := range haystack { - if s == needle { - return - } - } - t.Fatalf("expected %v to contain %q", haystack, needle) -} - -func mustNotContains(t *testing.T, s, sub string) { - t.Helper() - if strings.Contains(s, sub) { - t.Fatalf("expected %q to not contain %q", s, sub) - } -} - -func mustTrue(t *testing.T, cond bool) { - t.Helper() - if !cond { - t.Fatal("expected true") - } -} - -func mustFalse(t *testing.T, cond bool) { - t.Helper() - if cond { - t.Fatal("expected false") - } -} - -func mustLen[T any](t *testing.T, got []T, want int) { - t.Helper() - if len(got) != want { - t.Fatalf("want length %d, got %d", want, len(got)) - } -} - -func mustNotNil(t *testing.T, v any) { - t.Helper() - if v == nil { - t.Fatal("expected non-nil") - } -} diff --git a/cmd/docs/ax7_test.go b/cmd/docs/ax7_test.go new file mode 100644 index 0000000..3e17ca6 --- /dev/null +++ b/cmd/docs/ax7_test.go @@ -0,0 +1,38 @@ +package docs + +import ( + . "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func TestAX7_AddDocsCommands_Good(t *T) { + root := &cli.Command{Use: "root"} + AddDocsCommands(root) + commands := root.Commands() + + AssertLen(t, commands, 1) + AssertEqual(t, "docs", commands[0].Use) +} + +func TestAX7_AddDocsCommands_Bad(t *T) { + var root *cli.Command + AssertPanics(t, func() { + AddDocsCommands(root) + }) + AssertNil(t, root) +} + +func TestAX7_AddDocsCommands_Ugly(t *T) { + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddDocsCommands(root) + + foundExisting := false + foundDocs := false + for _, cmd := range root.Commands() { + foundExisting = foundExisting || cmd.Use == "existing" + foundDocs = foundDocs || cmd.Use == "docs" + } + AssertTrue(t, foundExisting) + AssertTrue(t, foundDocs) +} diff --git a/cmd/docs/cmd_scan.go b/cmd/docs/cmd_scan.go index d93278a..3891ab8 100644 --- a/cmd/docs/cmd_scan.go +++ b/cmd/docs/cmd_scan.go @@ -6,11 +6,11 @@ import ( "path/filepath" "strings" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/devops/cmd/workspace" "dappco.re/go/i18n" "dappco.re/go/io" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" ) // RepoDocInfo holds documentation info for a repo @@ -118,7 +118,7 @@ func scanRepoDocs(repo *repos.Repo) RepoDocInfo { docsDir := filepath.Join(repo.Path, "docs") // Check if directory exists by listing it if _, err := io.Local.List(docsDir); err == nil { - _ = filepath.WalkDir(docsDir, func(path string, d fs.DirEntry, err error) error { + if err := filepath.WalkDir(docsDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } @@ -131,28 +131,38 @@ func scanRepoDocs(repo *repos.Repo) RepoDocInfo { return nil } // Get relative path from docs/ - relPath, _ := filepath.Rel(docsDir, path) + relPath, err := filepath.Rel(docsDir, path) + if err != nil { + return err + } info.DocsFiles = append(info.DocsFiles, relPath) info.HasDocs = true return nil - }) + }); err != nil { + return info + } } // Recursively scan KB/ directory for .md files kbDir := filepath.Join(repo.Path, "KB") if _, err := io.Local.List(kbDir); err == nil { - _ = filepath.WalkDir(kbDir, func(path string, d fs.DirEntry, err error) error { + if err := filepath.WalkDir(kbDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() || !strings.HasSuffix(d.Name(), ".md") { return nil } - relPath, _ := filepath.Rel(kbDir, path) + relPath, err := filepath.Rel(kbDir, path) + if err != nil { + return err + } info.KBFiles = append(info.KBFiles, relPath) info.HasDocs = true return nil - }) + }); err != nil { + return info + } } return info diff --git a/cmd/docs/cmd_sync.go b/cmd/docs/cmd_sync.go index 41a9359..4cdf34b 100644 --- a/cmd/docs/cmd_sync.go +++ b/cmd/docs/cmd_sync.go @@ -6,10 +6,10 @@ import ( "path/filepath" "strings" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" "dappco.re/go/io" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" ) // Flag variables for sync command diff --git a/cmd/gitcmd/ax7_test.go b/cmd/gitcmd/ax7_test.go new file mode 100644 index 0000000..cfb452d --- /dev/null +++ b/cmd/gitcmd/ax7_test.go @@ -0,0 +1,39 @@ +package gitcmd + +import ( + . "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func TestAX7_AddGitCommands_Good(t *T) { + root := &cli.Command{Use: "root"} + AddGitCommands(root) + gitCmd := root.Commands()[0] + + AssertEqual(t, "git", gitCmd.Use) + AssertGreaterOrEqual(t, len(gitCmd.Commands()), 7) +} + +func TestAX7_AddGitCommands_Bad(t *T) { + var root *cli.Command + AssertPanics(t, func() { + AddGitCommands(root) + }) + AssertNil(t, root) +} + +func TestAX7_AddGitCommands_Ugly(t *T) { + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddGitCommands(root) + + foundExisting := false + foundGit := false + for _, cmd := range root.Commands() { + foundExisting = foundExisting || cmd.Use == "existing" + foundGit = foundGit || cmd.Use == "git" + } + AssertLen(t, root.Commands(), 2) + AssertTrue(t, foundExisting) + AssertTrue(t, foundGit) +} diff --git a/cmd/gitcmd/cmd_git.go b/cmd/gitcmd/cmd_git.go index e63d826..9629bb0 100644 --- a/cmd/gitcmd/cmd_git.go +++ b/cmd/gitcmd/cmd_git.go @@ -13,8 +13,8 @@ package gitcmd import ( - "dappco.re/go/devops/cmd/dev" "dappco.re/go/cli/pkg/cli" + "dappco.re/go/devops/cmd/dev" "dappco.re/go/i18n" ) diff --git a/cmd/setup/ax7_commands_test.go b/cmd/setup/ax7_commands_test.go new file mode 100644 index 0000000..88bbc32 --- /dev/null +++ b/cmd/setup/ax7_commands_test.go @@ -0,0 +1,84 @@ +package setup + +import ( + core "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func ax7ResetSetupCommand(t *core.T) { + original := setupCmd + setupCmd = &cli.Command{ + Use: "setup", + RunE: func(cmd *cli.Command, args []string) error { + return nil + }, + } + t.Cleanup(func() { setupCmd = original }) +} + +func ax7SetupCommand(root *cli.Command, use string) *cli.Command { + for _, cmd := range root.Commands() { + if cmd.Use == use || core.HasPrefix(cmd.Use, use+" ") { + return cmd + } + } + return nil +} + +func TestAX7_AddSetupCommand_Good(t *core.T) { + ax7ResetSetupCommand(t) + root := &cli.Command{Use: "root"} + AddSetupCommand(root) + cmd := ax7SetupCommand(root, "setup") + + core.AssertNotNil(t, cmd) + core.AssertNotNil(t, cmd.Flag("registry")) +} + +func TestAX7_AddSetupCommand_Bad(t *core.T) { + ax7ResetSetupCommand(t) + var root *cli.Command + core.AssertPanics(t, func() { + AddSetupCommand(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddSetupCommand_Ugly(t *core.T) { + ax7ResetSetupCommand(t) + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddSetupCommand(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7SetupCommand(root, "setup")) +} + +func TestAX7_AddSetupCommands_Good(t *core.T) { + ax7ResetSetupCommand(t) + root := &cli.Command{Use: "root"} + AddSetupCommands(root) + cmd := ax7SetupCommand(root, "setup") + + core.AssertNotNil(t, cmd) + core.AssertNotEmpty(t, cmd.Short) +} + +func TestAX7_AddSetupCommands_Bad(t *core.T) { + ax7ResetSetupCommand(t) + var root *cli.Command + core.AssertPanics(t, func() { + AddSetupCommands(root) + }) + core.AssertNil(t, root) +} + +func TestAX7_AddSetupCommands_Ugly(t *core.T) { + ax7ResetSetupCommand(t) + root := &cli.Command{Use: "root"} + root.AddCommand(&cli.Command{Use: "existing"}) + AddSetupCommands(root) + + core.AssertLen(t, root.Commands(), 2) + core.AssertNotNil(t, ax7SetupCommand(root, "setup")) +} diff --git a/cmd/setup/ax7_diff_config_test.go b/cmd/setup/ax7_diff_config_test.go new file mode 100644 index 0000000..51b4950 --- /dev/null +++ b/cmd/setup/ax7_diff_config_test.go @@ -0,0 +1,465 @@ +package setup + +import core "dappco.re/go" + +func TestAX7_NewChangeSet_Good(t *core.T) { + cs := NewChangeSet("repo-a") + core.AssertEqual(t, "repo-a", cs.Repo) + + core.AssertNotNil(t, cs.Changes) + core.AssertEmpty(t, cs.Changes) +} + +func TestAX7_NewChangeSet_Bad(t *core.T) { + cs := NewChangeSet("") + core.AssertEqual(t, "", cs.Repo) + + core.AssertNotNil(t, cs.Changes) + core.AssertFalse(t, cs.HasChanges()) +} + +func TestAX7_NewChangeSet_Ugly(t *core.T) { + first := NewChangeSet("repo-a") + second := NewChangeSet("repo-a") + first.Add(CategoryLabel, ChangeCreate, "bug", "create") + + core.AssertLen(t, first.Changes, 1) + core.AssertEmpty(t, second.Changes) +} + +func TestAX7_ChangeSet_Add_Good(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeCreate, "bug", "create bug label") + + core.AssertLen(t, cs.Changes, 1) + core.AssertEqual(t, ChangeCreate, cs.Changes[0].Type) +} + +func TestAX7_ChangeSet_Add_Bad(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeSkip, "bug", "up to date") + + core.AssertLen(t, cs.Changes, 1) + core.AssertFalse(t, cs.HasChanges()) +} + +func TestAX7_ChangeSet_Add_Ugly(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add("", "", "", "") + + core.AssertLen(t, cs.Changes, 1) + core.AssertEqual(t, ChangeType(""), cs.Changes[0].Type) +} + +func TestAX7_ChangeSet_AddWithDetails_Good(t *core.T) { + cs := NewChangeSet("repo-a") + cs.AddWithDetails(CategoryLabel, ChangeUpdate, "bug", "update", map[string]string{"color": "old -> new"}) + + core.AssertLen(t, cs.Changes, 1) + core.AssertEqual(t, "old -> new", cs.Changes[0].Details["color"]) +} + +func TestAX7_ChangeSet_AddWithDetails_Bad(t *core.T) { + cs := NewChangeSet("repo-a") + cs.AddWithDetails(CategoryLabel, ChangeUpdate, "bug", "update", nil) + + core.AssertLen(t, cs.Changes, 1) + core.AssertNil(t, cs.Changes[0].Details) +} + +func TestAX7_ChangeSet_AddWithDetails_Ugly(t *core.T) { + details := map[string]string{"events": "push -> pull_request"} + cs := NewChangeSet("repo-a") + cs.AddWithDetails(CategoryWebhook, ChangeUpdate, "ci", "", details) + + details["events"] = "mutated" + core.AssertEqual(t, "mutated", cs.Changes[0].Details["events"]) +} + +func TestAX7_ChangeSet_HasChanges_Good(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategorySecurity, ChangeCreate, "alerts", "enable") + + core.AssertTrue(t, cs.HasChanges()) + core.AssertLen(t, cs.Changes, 1) +} + +func TestAX7_ChangeSet_HasChanges_Bad(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategorySecurity, ChangeSkip, "alerts", "up to date") + + core.AssertFalse(t, cs.HasChanges()) + core.AssertLen(t, cs.Changes, 1) +} + +func TestAX7_ChangeSet_HasChanges_Ugly(t *core.T) { + cs := NewChangeSet("repo-a") + core.AssertFalse(t, cs.HasChanges()) + + cs.Add(CategorySecurity, ChangeDelete, "alerts", "disable") + core.AssertTrue(t, cs.HasChanges()) +} + +func TestAX7_ChangeSet_Count_Good(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeCreate, "a", "") + cs.Add(CategoryLabel, ChangeUpdate, "b", "") + cs.Add(CategoryLabel, ChangeDelete, "c", "") + cs.Add(CategoryLabel, ChangeSkip, "d", "") + + creates, updates, deletes, skips := cs.Count() + core.AssertEqual(t, 1, creates) + core.AssertEqual(t, 1, updates) + core.AssertEqual(t, 1, deletes) + core.AssertEqual(t, 1, skips) +} + +func TestAX7_ChangeSet_Count_Bad(t *core.T) { + cs := NewChangeSet("repo-a") + creates, updates, deletes, skips := cs.Count() + + core.AssertEqual(t, 0, creates) + core.AssertEqual(t, 0, updates+deletes+skips) +} + +func TestAX7_ChangeSet_Count_Ugly(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeType("custom"), "x", "") + creates, updates, deletes, skips := cs.Count() + + core.AssertEqual(t, 0, creates+updates+deletes+skips) + core.AssertLen(t, cs.Changes, 1) +} + +func TestAX7_ChangeSet_CountByCategory_Good(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeCreate, "bug", "") + cs.Add(CategorySecurity, ChangeUpdate, "alerts", "") + + counts := cs.CountByCategory() + core.AssertEqual(t, 1, counts[CategoryLabel]) + core.AssertEqual(t, 1, counts[CategorySecurity]) +} + +func TestAX7_ChangeSet_CountByCategory_Bad(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeSkip, "bug", "") + + counts := cs.CountByCategory() + core.AssertEmpty(t, counts) + core.AssertEqual(t, 0, counts[CategoryLabel]) +} + +func TestAX7_ChangeSet_CountByCategory_Ugly(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add("", ChangeCreate, "unknown", "") + + counts := cs.CountByCategory() + core.AssertEqual(t, 1, counts[""]) + core.AssertLen(t, counts, 1) +} + +func TestAX7_ChangeSet_Print_Good(t *core.T) { + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeCreate, "bug", "create") + out, err := captureStdout(t, func() error { cs.Print(true); return nil }) + + core.AssertNoError(t, err) + core.AssertContains(t, out, "repo-a") +} + +func TestAX7_ChangeSet_Print_Bad(t *core.T) { + cs := NewChangeSet("repo-a") + out, err := captureStdout(t, func() error { cs.Print(false); return nil }) + + core.AssertNoError(t, err) + core.AssertContains(t, out, "repo-a") +} + +func TestAX7_ChangeSet_Print_Ugly(t *core.T) { + cs := NewChangeSet("repo-a") + cs.AddWithDetails(CategoryWebhook, ChangeUpdate, "ci", "", map[string]string{"b": "2", "a": "1"}) + out, err := captureStdout(t, func() error { cs.Print(true); return nil }) + + core.AssertNoError(t, err) + core.AssertContains(t, out, "ci") +} + +func TestAX7_NewAggregate_Good(t *core.T) { + agg := NewAggregate() + core.AssertNotNil(t, agg) + + core.AssertNotNil(t, agg.Sets) + core.AssertEmpty(t, agg.Sets) +} + +func TestAX7_NewAggregate_Bad(t *core.T) { + agg := NewAggregate() + creates, updates, deletes, skips := agg.TotalChanges() + + core.AssertEqual(t, 0, creates) + core.AssertEqual(t, 0, updates+deletes+skips) +} + +func TestAX7_NewAggregate_Ugly(t *core.T) { + first := NewAggregate() + second := NewAggregate() + first.Add(NewChangeSet("repo-a")) + + core.AssertLen(t, first.Sets, 1) + core.AssertEmpty(t, second.Sets) +} + +func TestAX7_Aggregate_Add_Good(t *core.T) { + agg := NewAggregate() + cs := NewChangeSet("repo-a") + agg.Add(cs) + + core.AssertLen(t, agg.Sets, 1) + core.AssertEqual(t, cs, agg.Sets[0]) +} + +func TestAX7_Aggregate_Add_Bad(t *core.T) { + agg := NewAggregate() + agg.Add(nil) + + core.AssertLen(t, agg.Sets, 1) + core.AssertNil(t, agg.Sets[0]) +} + +func TestAX7_Aggregate_Add_Ugly(t *core.T) { + agg := NewAggregate() + cs := NewChangeSet("repo-a") + agg.Add(cs) + agg.Add(cs) + + core.AssertLen(t, agg.Sets, 2) + core.AssertEqual(t, cs, agg.Sets[1]) +} + +func TestAX7_Aggregate_TotalChanges_Good(t *core.T) { + agg := NewAggregate() + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeCreate, "bug", "") + cs.Add(CategoryLabel, ChangeUpdate, "ci", "") + agg.Add(cs) + + creates, updates, deletes, skips := agg.TotalChanges() + core.AssertEqual(t, 1, creates) + core.AssertEqual(t, 1, updates) + core.AssertEqual(t, 0, deletes+skips) +} + +func TestAX7_Aggregate_TotalChanges_Bad(t *core.T) { + agg := NewAggregate() + creates, updates, deletes, skips := agg.TotalChanges() + + core.AssertEqual(t, 0, creates) + core.AssertEqual(t, 0, updates+deletes+skips) +} + +func TestAX7_Aggregate_TotalChanges_Ugly(t *core.T) { + agg := NewAggregate() + empty := NewChangeSet("empty") + agg.Add(empty) + + creates, updates, deletes, skips := agg.TotalChanges() + core.AssertEqual(t, 0, creates+updates+deletes+skips) + core.AssertLen(t, agg.Sets, 1) +} + +func TestAX7_Aggregate_ReposWithChanges_Good(t *core.T) { + agg := NewAggregate() + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeCreate, "bug", "") + agg.Add(cs) + + core.AssertEqual(t, 1, agg.ReposWithChanges()) + core.AssertLen(t, agg.Sets, 1) +} + +func TestAX7_Aggregate_ReposWithChanges_Bad(t *core.T) { + agg := NewAggregate() + agg.Add(NewChangeSet("repo-a")) + + core.AssertEqual(t, 0, agg.ReposWithChanges()) + core.AssertLen(t, agg.Sets, 1) +} + +func TestAX7_Aggregate_ReposWithChanges_Ugly(t *core.T) { + agg := NewAggregate() + skipped := NewChangeSet("repo-a") + skipped.Add(CategoryLabel, ChangeSkip, "bug", "") + agg.Add(skipped) + + core.AssertEqual(t, 0, agg.ReposWithChanges()) + core.AssertFalse(t, skipped.HasChanges()) +} + +func TestAX7_Aggregate_PrintSummary_Good(t *core.T) { + agg := NewAggregate() + cs := NewChangeSet("repo-a") + cs.Add(CategoryLabel, ChangeCreate, "bug", "") + agg.Add(cs) + out, err := captureStdout(t, func() error { agg.PrintSummary(); return nil }) + + core.AssertNoError(t, err) + core.AssertContains(t, out, "1") +} + +func TestAX7_Aggregate_PrintSummary_Bad(t *core.T) { + agg := NewAggregate() + out, err := captureStdout(t, func() error { agg.PrintSummary(); return nil }) + + core.AssertNoError(t, err) + core.AssertContains(t, out, "0") +} + +func TestAX7_Aggregate_PrintSummary_Ugly(t *core.T) { + agg := NewAggregate() + skipped := NewChangeSet("repo-a") + skipped.Add(CategoryLabel, ChangeSkip, "bug", "") + agg.Add(skipped) + out, err := captureStdout(t, func() error { agg.PrintSummary(); return nil }) + + core.AssertNoError(t, err) + core.AssertContains(t, out, "1") +} + +func TestAX7_DefaultCIConfig_Good(t *core.T) { + cfg := DefaultCIConfig() + core.AssertEqual(t, "host-uk/tap", cfg.Tap) + + core.AssertEqual(t, "core", cfg.Formula) + core.AssertEqual(t, "dev", cfg.DefaultVersion) +} + +func TestAX7_DefaultCIConfig_Bad(t *core.T) { + cfg := DefaultCIConfig() + cfg.DefaultVersion = "" + + core.AssertEqual(t, "", cfg.DefaultVersion) + core.AssertEqual(t, "core-cli", cfg.ChocolateyPkg) +} + +func TestAX7_DefaultCIConfig_Ugly(t *core.T) { + first := DefaultCIConfig() + second := DefaultCIConfig() + first.Formula = "mutated" + + core.AssertEqual(t, "mutated", first.Formula) + core.AssertEqual(t, "core", second.Formula) +} + +func TestAX7_LoadCIConfig_Good(t *core.T) { + dir := t.TempDir() + core.RequireTrue(t, core.MkdirAll(core.Path(dir, ".core"), 0o755).OK) + core.RequireTrue(t, core.WriteFile(core.Path(dir, ".core", "ci.yaml"), []byte("formula: devops\ndefault_version: v1\n"), 0o644).OK) + t.Chdir(dir) + + cfg := LoadCIConfig() + core.AssertEqual(t, "devops", cfg.Formula) + core.AssertEqual(t, "v1", cfg.DefaultVersion) +} + +func TestAX7_LoadCIConfig_Bad(t *core.T) { + dir := t.TempDir() + t.Chdir(dir) + cfg := LoadCIConfig() + + core.AssertEqual(t, "core", cfg.Formula) + core.AssertEqual(t, "dev", cfg.DefaultVersion) +} + +func TestAX7_LoadCIConfig_Ugly(t *core.T) { + dir := t.TempDir() + child := core.Path(dir, "a", "b") + core.RequireTrue(t, core.MkdirAll(core.Path(dir, ".core"), 0o755).OK) + core.RequireTrue(t, core.MkdirAll(child, 0o755).OK) + core.RequireTrue(t, core.WriteFile(core.Path(dir, ".core", "ci.yaml"), []byte("tap: custom/tap\n"), 0o644).OK) + t.Chdir(child) + + cfg := LoadCIConfig() + core.AssertEqual(t, "custom/tap", cfg.Tap) + core.AssertEqual(t, "core", cfg.Formula) +} + +func TestAX7_LoadGitHubConfig_Good(t *core.T) { + path := core.Path(t.TempDir(), "github.yaml") + t.Setenv("HOOK_URL", "https://hooks.example") + core.RequireTrue(t, core.WriteFile(path, []byte("version: 1\nlabels:\n- name: bug\n color: ff0000\nwebhooks:\n ci:\n url: ${HOOK_URL}\n events: [push]\n"), 0o644).OK) + + cfg, err := LoadGitHubConfig(path) + core.AssertNoError(t, err) + core.AssertEqual(t, "https://hooks.example", cfg.Webhooks["ci"].URL) +} + +func TestAX7_LoadGitHubConfig_Bad(t *core.T) { + cfg, err := LoadGitHubConfig(core.Path(t.TempDir(), "missing.yaml")) + core.AssertError(t, err) + + core.AssertNil(t, cfg) + core.AssertContains(t, err.Error(), "failed to read") +} + +func TestAX7_LoadGitHubConfig_Ugly(t *core.T) { + path := core.Path(t.TempDir(), "github.yaml") + core.RequireTrue(t, core.WriteFile(path, []byte("version: 1\nwebhooks:\n ci:\n url: ${MISSING_URL:-https://fallback.example}\n events: [push]\n"), 0o644).OK) + + cfg, err := LoadGitHubConfig(path) + core.AssertNoError(t, err) + core.AssertEqual(t, "https://fallback.example", cfg.Webhooks["ci"].URL) +} + +func TestAX7_FindGitHubConfig_Good(t *core.T) { + dir := t.TempDir() + path := core.Path(dir, ".core", "github.yaml") + core.RequireTrue(t, core.MkdirAll(core.Path(dir, ".core"), 0o755).OK) + core.RequireTrue(t, core.WriteFile(path, []byte("version: 1\n"), 0o644).OK) + + got, err := FindGitHubConfig(dir, "") + core.AssertNoError(t, err) + core.AssertEqual(t, path, got) +} + +func TestAX7_FindGitHubConfig_Bad(t *core.T) { + got, err := FindGitHubConfig(t.TempDir(), "missing.yaml") + core.AssertError(t, err) + + core.AssertEqual(t, "", got) + core.AssertContains(t, err.Error(), "config file not found") +} + +func TestAX7_FindGitHubConfig_Ugly(t *core.T) { + dir := t.TempDir() + path := core.Path(dir, "github.yaml") + core.RequireTrue(t, core.WriteFile(path, []byte("version: 1\n"), 0o644).OK) + + got, err := FindGitHubConfig(dir, "") + core.AssertNoError(t, err) + core.AssertEqual(t, path, got) +} + +func TestAX7_GitHubConfig_Validate_Good(t *core.T) { + cfg := &GitHubConfig{Version: 1, Labels: []LabelConfig{{Name: "bug", Color: "ff0000"}}} + err := cfg.Validate() + + core.AssertNoError(t, err) + core.AssertEqual(t, 1, cfg.Version) +} + +func TestAX7_GitHubConfig_Validate_Bad(t *core.T) { + cfg := &GitHubConfig{Version: 2} + err := cfg.Validate() + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "unsupported") +} + +func TestAX7_GitHubConfig_Validate_Ugly(t *core.T) { + cfg := &GitHubConfig{Version: 1, Labels: []LabelConfig{{Name: "bug", Color: "nope"}}} + err := cfg.Validate() + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "invalid color") +} diff --git a/cmd/setup/ax7_github_test.go b/cmd/setup/ax7_github_test.go new file mode 100644 index 0000000..22b169f --- /dev/null +++ b/cmd/setup/ax7_github_test.go @@ -0,0 +1,462 @@ +package setup + +import core "dappco.re/go" + +func ax7FakeGH(t *core.T, body string) { + dir := t.TempDir() + path := core.Path(dir, "gh") + script := "#!/bin/sh\n" + body + "\n" + core.RequireTrue(t, core.WriteFile(path, []byte(script), 0o755).OK) + t.Setenv("PATH", dir+":"+core.Getenv("PATH")) +} + +func ax7GHHappy(t *core.T) { + ax7FakeGH(t, ` +if [ "$1" = "label" ] && [ "$2" = "list" ]; then + echo '[{"name":"bug","color":"00ff00","description":"old"}]' + exit 0 +fi +if [ "$1" = "label" ]; then + exit 0 +fi +if [ "$1" = "api" ]; then + case "$2" in + repos/*/*/hooks) + if [ "$3" = "--method" ]; then exit 0; fi + echo '[{"id":7,"name":"web","active":true,"events":["push"],"config":{"url":"https://hooks.example","content_type":"json","insecure_ssl":"0"}}]' + exit 0 + ;; + repos/*/*/hooks/*) + exit 0 + ;; + repos/*/*/branches/*/protection) + if [ "$3" = "--method" ]; then exit 0; fi + echo '{"required_status_checks":{"strict":true,"contexts":["test"]},"required_pull_request_reviews":{"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"required_approving_review_count":1},"enforce_admins":{"enabled":true},"required_linear_history":{"enabled":false},"allow_force_pushes":{"enabled":false},"allow_deletions":{"enabled":false},"required_conversation_resolution":{"enabled":false}}' + exit 0 + ;; + repos/*/*/vulnerability-alerts) + exit 0 + ;; + repos/*/*/automated-security-fixes) + exit 0 + ;; + repos/*/*) + echo '{"security_and_analysis":{"secret_scanning":{"status":"disabled"},"secret_scanning_push_protection":{"status":"disabled"},"dependabot_security_updates":{"status":"disabled"}}}' + exit 0 + ;; + esac +fi +echo '{}' +`) +} + +func TestAX7_ListLabels_Good(t *core.T) { + ax7GHHappy(t) + labels, err := ListLabels("owner/repo") + + core.AssertNoError(t, err) + core.AssertEqual(t, "bug", labels[0].Name) +} + +func TestAX7_ListLabels_Bad(t *core.T) { + ax7FakeGH(t, "echo label failure >&2\nexit 1") + labels, err := ListLabels("owner/repo") + + core.AssertError(t, err) + core.AssertNil(t, labels) +} + +func TestAX7_ListLabels_Ugly(t *core.T) { + ax7FakeGH(t, "echo '[]'\nexit 0") + labels, err := ListLabels("owner/repo") + + core.AssertNoError(t, err) + core.AssertEmpty(t, labels) +} + +func TestAX7_CreateLabel_Good(t *core.T) { + ax7GHHappy(t) + err := CreateLabel("owner/repo", LabelConfig{Name: "bug", Color: "ff0000", Description: "Bug"}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_CreateLabel_Bad(t *core.T) { + ax7FakeGH(t, "echo create failed\nexit 1") + err := CreateLabel("owner/repo", LabelConfig{Name: "bug", Color: "ff0000"}) + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "create failed") +} + +func TestAX7_CreateLabel_Ugly(t *core.T) { + ax7GHHappy(t) + err := CreateLabel("owner/repo", LabelConfig{Name: "empty-description", Color: "000000"}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_EditLabel_Good(t *core.T) { + ax7GHHappy(t) + err := EditLabel("owner/repo", LabelConfig{Name: "bug", Color: "ff0000", Description: "Bug"}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_EditLabel_Bad(t *core.T) { + ax7FakeGH(t, "echo edit failed\nexit 1") + err := EditLabel("owner/repo", LabelConfig{Name: "bug", Color: "ff0000"}) + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "edit failed") +} + +func TestAX7_EditLabel_Ugly(t *core.T) { + ax7GHHappy(t) + err := EditLabel("owner/repo", LabelConfig{Name: "empty-description", Color: "000000"}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_SyncLabels_Good(t *core.T) { + ax7GHHappy(t) + cfg := &GitHubConfig{Labels: []LabelConfig{{Name: "bug", Color: "ff0000", Description: "new"}}} + changes, err := SyncLabels("owner/repo", cfg, true) + + core.AssertNoError(t, err) + core.AssertEqual(t, ChangeUpdate, changes.Changes[0].Type) +} + +func TestAX7_SyncLabels_Bad(t *core.T) { + ax7FakeGH(t, "echo list failed >&2\nexit 1") + changes, err := SyncLabels("owner/repo", &GitHubConfig{}, true) + + core.AssertError(t, err) + core.AssertNil(t, changes) +} + +func TestAX7_SyncLabels_Ugly(t *core.T) { + ax7GHHappy(t) + changes, err := SyncLabels("owner/repo", &GitHubConfig{}, true) + + core.AssertNoError(t, err) + core.AssertFalse(t, changes.HasChanges()) +} + +func TestAX7_ListWebhooks_Good(t *core.T) { + ax7GHHappy(t) + hooks, err := ListWebhooks("owner/repo") + + core.AssertNoError(t, err) + core.AssertEqual(t, 7, hooks[0].ID) +} + +func TestAX7_ListWebhooks_Bad(t *core.T) { + hooks, err := ListWebhooks("invalid") + core.AssertError(t, err) + + core.AssertNil(t, hooks) + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_ListWebhooks_Ugly(t *core.T) { + ax7FakeGH(t, "echo '[]'\nexit 0") + hooks, err := ListWebhooks("owner/repo") + + core.AssertNoError(t, err) + core.AssertEmpty(t, hooks) +} + +func TestAX7_CreateWebhook_Good(t *core.T) { + ax7GHHappy(t) + err := CreateWebhook("owner/repo", "ci", WebhookConfig{URL: "https://hooks.example", ContentType: "json", Events: []string{"push"}}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_CreateWebhook_Bad(t *core.T) { + err := CreateWebhook("invalid", "ci", WebhookConfig{}) + core.AssertError(t, err) + + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_CreateWebhook_Ugly(t *core.T) { + ax7GHHappy(t) + active := false + err := CreateWebhook("owner/repo", "ci", WebhookConfig{URL: "https://hooks.example", Secret: "secret", Active: &active}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_UpdateWebhook_Good(t *core.T) { + ax7GHHappy(t) + err := UpdateWebhook("owner/repo", 7, WebhookConfig{URL: "https://hooks.example", ContentType: "json", Events: []string{"push"}}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_UpdateWebhook_Bad(t *core.T) { + err := UpdateWebhook("invalid", 7, WebhookConfig{}) + core.AssertError(t, err) + + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_UpdateWebhook_Ugly(t *core.T) { + ax7GHHappy(t) + active := false + err := UpdateWebhook("owner/repo", 0, WebhookConfig{URL: "", Active: &active}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_SyncWebhooks_Good(t *core.T) { + ax7GHHappy(t) + cfg := &GitHubConfig{Webhooks: map[string]WebhookConfig{"ci": {URL: "https://hooks.example", ContentType: "form", Events: []string{"push"}}}} + changes, err := SyncWebhooks("owner/repo", cfg, true) + + core.AssertNoError(t, err) + core.AssertEqual(t, ChangeUpdate, changes.Changes[0].Type) +} + +func TestAX7_SyncWebhooks_Bad(t *core.T) { + changes, err := SyncWebhooks("invalid", &GitHubConfig{Webhooks: map[string]WebhookConfig{"ci": {URL: "x"}}}, true) + core.AssertError(t, err) + + core.AssertNil(t, changes) + core.AssertContains(t, err.Error(), "failed to list") +} + +func TestAX7_SyncWebhooks_Ugly(t *core.T) { + changes, err := SyncWebhooks("owner/repo", &GitHubConfig{}, true) + core.AssertNoError(t, err) + + core.AssertFalse(t, changes.HasChanges()) + core.AssertEmpty(t, changes.Changes) +} + +func TestAX7_GetBranchProtection_Good(t *core.T) { + ax7GHHappy(t) + protection, err := GetBranchProtection("owner/repo", "main") + + core.AssertNoError(t, err) + core.AssertEqual(t, 1, protection.RequiredPullRequestReviews.RequiredApprovingReviewCount) +} + +func TestAX7_GetBranchProtection_Bad(t *core.T) { + protection, err := GetBranchProtection("invalid", "main") + core.AssertError(t, err) + + core.AssertNil(t, protection) + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_GetBranchProtection_Ugly(t *core.T) { + ax7FakeGH(t, "echo '404 Branch not protected' >&2\nexit 1") + protection, err := GetBranchProtection("owner/repo", "main") + + core.AssertNoError(t, err) + core.AssertNil(t, protection) +} + +func TestAX7_SetBranchProtection_Good(t *core.T) { + ax7GHHappy(t) + err := SetBranchProtection("owner/repo", "main", BranchProtectionConfig{RequiredReviews: 1, DismissStale: true}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_SetBranchProtection_Bad(t *core.T) { + err := SetBranchProtection("invalid", "main", BranchProtectionConfig{}) + core.AssertError(t, err) + + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_SetBranchProtection_Ugly(t *core.T) { + ax7GHHappy(t) + err := SetBranchProtection("owner/repo", "", BranchProtectionConfig{}) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_SyncBranchProtection_Good(t *core.T) { + ax7GHHappy(t) + cfg := &GitHubConfig{BranchProtection: []BranchProtectionConfig{{Branch: "main", RequiredReviews: 2}}} + changes, err := SyncBranchProtection("owner/repo", cfg, true) + + core.AssertNoError(t, err) + core.AssertEqual(t, ChangeUpdate, changes.Changes[0].Type) +} + +func TestAX7_SyncBranchProtection_Bad(t *core.T) { + changes, err := SyncBranchProtection("invalid", &GitHubConfig{BranchProtection: []BranchProtectionConfig{{Branch: "main"}}}, true) + core.AssertError(t, err) + + core.AssertNil(t, changes) + core.AssertContains(t, err.Error(), "failed to get") +} + +func TestAX7_SyncBranchProtection_Ugly(t *core.T) { + changes, err := SyncBranchProtection("owner/repo", &GitHubConfig{}, true) + core.AssertNoError(t, err) + + core.AssertFalse(t, changes.HasChanges()) + core.AssertEmpty(t, changes.Changes) +} + +func TestAX7_GetSecuritySettings_Good(t *core.T) { + ax7GHHappy(t) + status, err := GetSecuritySettings("owner/repo") + + core.AssertNoError(t, err) + core.AssertTrue(t, status.DependabotAlerts) +} + +func TestAX7_GetSecuritySettings_Bad(t *core.T) { + status, err := GetSecuritySettings("invalid") + core.AssertError(t, err) + + core.AssertNil(t, status) + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_GetSecuritySettings_Ugly(t *core.T) { + ax7FakeGH(t, ` +if [ "$2" = "repos/owner/repo/vulnerability-alerts" ]; then echo 404 >&2; exit 1; fi +echo '{"security_and_analysis":{"secret_scanning":{"status":"enabled"},"secret_scanning_push_protection":{"status":"enabled"},"dependabot_security_updates":{"status":"enabled"}}}' +`) + status, err := GetSecuritySettings("owner/repo") + + core.AssertNoError(t, err) + core.AssertFalse(t, status.DependabotAlerts) + core.AssertTrue(t, status.SecretScanning) +} + +func TestAX7_EnableDependabotAlerts_Good(t *core.T) { + ax7GHHappy(t) + err := EnableDependabotAlerts("owner/repo") + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_EnableDependabotAlerts_Bad(t *core.T) { + err := EnableDependabotAlerts("invalid") + core.AssertError(t, err) + + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_EnableDependabotAlerts_Ugly(t *core.T) { + ax7FakeGH(t, "exit 0") + err := EnableDependabotAlerts("owner/repo") + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_EnableDependabotSecurityUpdates_Good(t *core.T) { + ax7GHHappy(t) + err := EnableDependabotSecurityUpdates("owner/repo") + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_EnableDependabotSecurityUpdates_Bad(t *core.T) { + err := EnableDependabotSecurityUpdates("invalid") + core.AssertError(t, err) + + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_EnableDependabotSecurityUpdates_Ugly(t *core.T) { + ax7FakeGH(t, "exit 0") + err := EnableDependabotSecurityUpdates("owner/repo") + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_DisableDependabotSecurityUpdates_Good(t *core.T) { + ax7GHHappy(t) + err := DisableDependabotSecurityUpdates("owner/repo") + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_DisableDependabotSecurityUpdates_Bad(t *core.T) { + err := DisableDependabotSecurityUpdates("invalid") + core.AssertError(t, err) + + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_DisableDependabotSecurityUpdates_Ugly(t *core.T) { + ax7FakeGH(t, "exit 0") + err := DisableDependabotSecurityUpdates("owner/repo") + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_UpdateSecurityAndAnalysis_Good(t *core.T) { + ax7GHHappy(t) + err := UpdateSecurityAndAnalysis("owner/repo", true, true) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_UpdateSecurityAndAnalysis_Bad(t *core.T) { + err := UpdateSecurityAndAnalysis("invalid", true, true) + core.AssertError(t, err) + + core.AssertContains(t, err.Error(), "invalid repo format") +} + +func TestAX7_UpdateSecurityAndAnalysis_Ugly(t *core.T) { + ax7FakeGH(t, "echo 'secret scanning not available'\nexit 1") + err := UpdateSecurityAndAnalysis("owner/repo", true, true) + + core.AssertNoError(t, err) + core.AssertTrue(t, err == nil) +} + +func TestAX7_SyncSecuritySettings_Good(t *core.T) { + ax7GHHappy(t) + cfg := &GitHubConfig{Security: SecurityConfig{DependabotSecurityUpdates: true, SecretScanning: true}} + changes, err := SyncSecuritySettings("owner/repo", cfg, true) + + core.AssertNoError(t, err) + core.AssertTrue(t, changes.HasChanges()) +} + +func TestAX7_SyncSecuritySettings_Bad(t *core.T) { + changes, err := SyncSecuritySettings("invalid", &GitHubConfig{}, true) + core.AssertError(t, err) + + core.AssertNil(t, changes) + core.AssertContains(t, err.Error(), "failed to get") +} + +func TestAX7_SyncSecuritySettings_Ugly(t *core.T) { + ax7GHHappy(t) + changes, err := SyncSecuritySettings("owner/repo", &GitHubConfig{}, true) + + core.AssertNoError(t, err) + core.AssertFalse(t, changes.HasChanges()) +} diff --git a/cmd/setup/cmd_ci.go b/cmd/setup/cmd_ci.go index 2dd35b7..bb45ddb 100644 --- a/cmd/setup/cmd_ci.go +++ b/cmd/setup/cmd_ci.go @@ -6,8 +6,8 @@ import ( "path/filepath" "runtime" - coreio "dappco.re/go/io" "dappco.re/go/cli/pkg/cli" + coreio "dappco.re/go/io" "gopkg.in/yaml.v3" ) diff --git a/cmd/setup/cmd_ci_test.go b/cmd/setup/cmd_ci_test.go index 2f9d8bc..4715f86 100644 --- a/cmd/setup/cmd_ci_test.go +++ b/cmd/setup/cmd_ci_test.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "os" + "strings" "testing" ) @@ -12,7 +13,9 @@ func captureStdout(t *testing.T, fn func() error) (string, error) { oldStdout := os.Stdout r, w, err := os.Pipe() - mustNoError(t, err) + if err != nil { + t.Fatalf("create pipe: %v", err) + } defer func() { _ = r.Close() }() @@ -34,8 +37,12 @@ func captureStdout(t *testing.T, fn func() error) (string, error) { runErr := fn() - mustNoError(t, w.Close()) - mustNoError(t, <-errC) + if err := w.Close(); err != nil { + t.Fatalf("close pipe writer: %v", err) + } + if err := <-errC; err != nil { + t.Fatalf("copy stdout: %v", err) + } out := <-outC return out, runErr @@ -44,19 +51,35 @@ func captureStdout(t *testing.T, fn func() error) (string, error) { func TestDefaultCIConfig_Good(t *testing.T) { cfg := DefaultCIConfig() - mustEqual(t, "host-uk/tap", cfg.Tap) - mustEqual(t, "core", cfg.Formula) - mustEqual(t, "https://forge.lthn.ai/core/scoop-bucket.git", cfg.ScoopBucket) - mustEqual(t, "core-cli", cfg.ChocolateyPkg) - mustEqual(t, "host-uk/core", cfg.Repository) - mustEqual(t, "dev", cfg.DefaultVersion) + checks := map[string]struct { + got string + want string + }{ + "tap": {got: cfg.Tap, want: "host-uk/tap"}, + "formula": {got: cfg.Formula, want: "core"}, + "scoop bucket": {got: cfg.ScoopBucket, want: "https://forge.lthn.ai/core/scoop-bucket.git"}, + "chocolatey pkg": {got: cfg.ChocolateyPkg, want: "core-cli"}, + "repository": {got: cfg.Repository, want: "host-uk/core"}, + "default version": {got: cfg.DefaultVersion, want: "dev"}, + } + for name, check := range checks { + if check.got != check.want { + t.Fatalf("%s = %q, want %q", name, check.got, check.want) + } + } } func TestOutputPowershellInstall_Good(t *testing.T) { out, err := captureStdout(t, func() error { return outputPowershellInstall(DefaultCIConfig(), "dev") }) - mustNoError(t, err) - mustContains(t, out, `scoop bucket add host-uk $ScoopBucket`) - mustNotContains(t, out, `https://https://forge.lthn.ai/core/scoop-bucket.git`) + if err != nil { + t.Fatalf("output powershell install: %v", err) + } + if !strings.Contains(out, `scoop bucket add host-uk $ScoopBucket`) { + t.Fatalf("output missing scoop bucket command: %q", out) + } + if strings.Contains(out, `https://https://forge.lthn.ai/core/scoop-bucket.git`) { + t.Fatalf("output contains doubled URL scheme: %q", out) + } } diff --git a/cmd/setup/cmd_registry.go b/cmd/setup/cmd_registry.go index 7087fc2..e3f5cbb 100644 --- a/cmd/setup/cmd_registry.go +++ b/cmd/setup/cmd_registry.go @@ -13,12 +13,12 @@ import ( "path/filepath" "strings" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/devops/cmd/workspace" "dappco.re/go/i18n" coreio "dappco.re/go/io" log "dappco.re/go/log" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" ) // runRegistrySetup loads a registry from path and runs setup. diff --git a/cmd/setup/cmd_repo.go b/cmd/setup/cmd_repo.go index 8458530..aadd71a 100644 --- a/cmd/setup/cmd_repo.go +++ b/cmd/setup/cmd_repo.go @@ -14,10 +14,10 @@ import ( "path/filepath" "strings" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" coreio "dappco.re/go/io" log "dappco.re/go/log" - "dappco.re/go/cli/pkg/cli" ) var repoDryRun bool diff --git a/cmd/setup/cmd_repo_test.go b/cmd/setup/cmd_repo_test.go index 00ebc59..18788a7 100644 --- a/cmd/setup/cmd_repo_test.go +++ b/cmd/setup/cmd_repo_test.go @@ -8,23 +8,34 @@ import ( func TestRunRepoSetup_CreatesCoreConfigs_Good(t *testing.T) { dir := t.TempDir() - mustNoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n"), 0o644)) + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n"), 0o644); err != nil { + t.Fatalf("write go.mod: %v", err) + } - mustNoError(t, runRepoSetup(dir, false)) + if err := runRepoSetup(dir, false); err != nil { + t.Fatalf("run repo setup: %v", err) + } for _, name := range []string{"build.yaml", "release.yaml", "test.yaml"} { path := filepath.Join(dir, ".core", name) - _, err := os.Stat(path) - mustNoErrorf(t, err, "expected %s to exist", path) + if _, err := os.Stat(path); err != nil { + t.Fatalf("expected %s to exist: %v", path, err) + } } } func TestDetectProjectType_PrefersPackageOverComposer_Good(t *testing.T) { dir := t.TempDir() - mustNoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}\n"), 0o644)) - mustNoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{}\n"), 0o644)) + if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}\n"), 0o644); err != nil { + t.Fatalf("write package.json: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{}\n"), 0o644); err != nil { + t.Fatalf("write composer.json: %v", err) + } - mustEqual(t, "node", detectProjectType(dir)) + if got := detectProjectType(dir); got != "node" { + t.Fatalf("detectProjectType = %q, want %q", got, "node") + } } func TestParseGitHubRepoURL_Good(t *testing.T) { @@ -44,7 +55,9 @@ func TestParseGitHubRepoURL_Good(t *testing.T) { for remote, expected := range cases { t.Run(remote, func(t *testing.T) { - mustEqual(t, expected, parseGitHubRepoURL(remote)) + if got := parseGitHubRepoURL(remote); got != expected { + t.Fatalf("parseGitHubRepoURL(%q) = %q, want %q", remote, got, expected) + } }) } } diff --git a/cmd/setup/cmd_setup.go b/cmd/setup/cmd_setup.go index a337955..9a045b9 100644 --- a/cmd/setup/cmd_setup.go +++ b/cmd/setup/cmd_setup.go @@ -2,8 +2,8 @@ package setup import ( - "dappco.re/go/i18n" "dappco.re/go/cli/pkg/cli" + "dappco.re/go/i18n" ) // Style aliases from shared package diff --git a/cmd/setup/cmd_wizard.go b/cmd/setup/cmd_wizard.go index 1a9c96e..4d54e23 100644 --- a/cmd/setup/cmd_wizard.go +++ b/cmd/setup/cmd_wizard.go @@ -7,9 +7,9 @@ import ( "os" "slices" + "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" "dappco.re/go/scm/repos" - "dappco.re/go/cli/pkg/cli" "golang.org/x/term" ) diff --git a/cmd/setup/cmd_wizard_test.go b/cmd/setup/cmd_wizard_test.go index 2fac324..00452d2 100644 --- a/cmd/setup/cmd_wizard_test.go +++ b/cmd/setup/cmd_wizard_test.go @@ -1,6 +1,7 @@ package setup import ( + "slices" "testing" "dappco.re/go/scm/repos" @@ -15,9 +16,15 @@ func TestFilterReposByTypes_Good(t *testing.T) { filtered := filterReposByTypes(reposList, []string{"module", "product"}) - mustLen(t, filtered, 2) - mustEqual(t, "module-a", filtered[0].Name) - mustEqual(t, "product-a", filtered[1].Name) + if len(filtered) != 2 { + t.Fatalf("filtered length = %d, want 2", len(filtered)) + } + if filtered[0].Name != "module-a" { + t.Fatalf("filtered[0].Name = %q, want %q", filtered[0].Name, "module-a") + } + if filtered[1].Name != "product-a" { + t.Fatalf("filtered[1].Name = %q, want %q", filtered[1].Name, "product-a") + } } func TestFilterReposByTypes_EmptyFilter_Good(t *testing.T) { @@ -28,6 +35,10 @@ func TestFilterReposByTypes_EmptyFilter_Good(t *testing.T) { filtered := filterReposByTypes(reposList, nil) - mustLen(t, filtered, 2) - mustDeepEqual(t, reposList, filtered) + if len(filtered) != 2 { + t.Fatalf("filtered length = %d, want 2", len(filtered)) + } + if !slices.Equal(filtered, reposList) { + t.Fatalf("filtered = %v, want %v", filtered, reposList) + } } diff --git a/cmd/setup/test_helpers_test.go b/cmd/setup/test_helpers_test.go deleted file mode 100644 index c2cc3b0..0000000 --- a/cmd/setup/test_helpers_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package setup - -import ( - "reflect" - "strings" - "testing" -) - -func mustNoError(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func mustNoErrorf(t *testing.T, err error, format string, args ...any) { - t.Helper() - if err != nil { - t.Fatalf(format+": %v", append(args, err)...) - } -} - -func mustEqual[T comparable](t *testing.T, want, got T) { - t.Helper() - if want != got { - t.Fatalf("want %v, got %v", want, got) - } -} - -func mustDeepEqual(t *testing.T, want, got any) { - t.Helper() - if !reflect.DeepEqual(want, got) { - t.Fatalf("want %v, got %v", want, got) - } -} - -func mustContains(t *testing.T, s, sub string) { - t.Helper() - if !strings.Contains(s, sub) { - t.Fatalf("expected %q to contain %q", s, sub) - } -} - -func mustNotContains(t *testing.T, s, sub string) { - t.Helper() - if strings.Contains(s, sub) { - t.Fatalf("expected %q to not contain %q", s, sub) - } -} - -func mustLen[T any](t *testing.T, got []T, want int) { - t.Helper() - if len(got) != want { - t.Fatalf("want length %d, got %d", want, len(got)) - } -} - -func mustContainsString(t *testing.T, haystack []string, needle string) { - t.Helper() - for _, s := range haystack { - if s == needle { - return - } - } - t.Fatalf("expected %v to contain %q", haystack, needle) -} diff --git a/cmd/workspace/ax7_test.go b/cmd/workspace/ax7_test.go new file mode 100644 index 0000000..e00968a --- /dev/null +++ b/cmd/workspace/ax7_test.go @@ -0,0 +1,92 @@ +package workspace + +import . "dappco.re/go" + +func TestAX7_DefaultConfig_Good(t *T) { + cfg := DefaultConfig() + AssertNotNil(t, cfg) + + AssertEqual(t, 1, cfg.Version) + AssertEqual(t, "./packages", cfg.PackagesDir) +} + +func TestAX7_DefaultConfig_Bad(t *T) { + cfg := DefaultConfig() + cfg.PackagesDir = "" + + AssertEqual(t, "", cfg.PackagesDir) + AssertEqual(t, 1, cfg.Version) +} + +func TestAX7_DefaultConfig_Ugly(t *T) { + first := DefaultConfig() + second := DefaultConfig() + first.PackagesDir = "changed" + + AssertEqual(t, "changed", first.PackagesDir) + AssertEqual(t, "./packages", second.PackagesDir) +} + +func TestAX7_LoadConfig_Good(t *T) { + dir := t.TempDir() + RequireTrue(t, MkdirAll(Path(dir, ".core"), 0o755).OK) + RequireTrue(t, WriteFile(Path(dir, ".core", "workspace.yaml"), []byte("version: 1\nactive: devops\npackages_dir: repos\n"), 0o644).OK) + + cfg, err := LoadConfig(dir) + AssertNoError(t, err) + AssertEqual(t, "devops", cfg.Active) + AssertEqual(t, "repos", cfg.PackagesDir) +} + +func TestAX7_LoadConfig_Bad(t *T) { + cfg, err := LoadConfig(t.TempDir()) + AssertNoError(t, err) + + AssertNil(t, cfg) + AssertNoError(t, err) +} + +func TestAX7_LoadConfig_Ugly(t *T) { + dir := t.TempDir() + child := Path(dir, "a", "b") + RequireTrue(t, MkdirAll(Path(dir, ".core"), 0o755).OK) + RequireTrue(t, MkdirAll(child, 0o755).OK) + RequireTrue(t, WriteFile(Path(dir, ".core", "workspace.yaml"), []byte("version: 1\npackages_dir: nested\n"), 0o644).OK) + + cfg, err := LoadConfig(child) + AssertNoError(t, err) + AssertEqual(t, "nested", cfg.PackagesDir) +} + +func TestAX7_FindRoot_Good(t *T) { + dir := t.TempDir() + RequireTrue(t, MkdirAll(Path(dir, ".core"), 0o755).OK) + RequireTrue(t, WriteFile(Path(dir, ".core", "workspace.yaml"), []byte("version: 1\n"), 0o644).OK) + t.Chdir(dir) + + root, err := FindRoot() + AssertNoError(t, err) + AssertEqual(t, dir, root) +} + +func TestAX7_FindRoot_Bad(t *T) { + dir := t.TempDir() + t.Chdir(dir) + + root, err := FindRoot() + AssertError(t, err) + AssertEqual(t, "", root) +} + +func TestAX7_FindRoot_Ugly(t *T) { + dir := t.TempDir() + child := Path(dir, "nested", "deep") + RequireTrue(t, MkdirAll(Path(dir, ".core"), 0o755).OK) + RequireTrue(t, MkdirAll(child, 0o755).OK) + RequireTrue(t, WriteFile(Path(dir, ".core", "workspace.yaml"), []byte("version: 1\n"), 0o644).OK) + t.Chdir(child) + + root, err := FindRoot() + AssertNoError(t, err) + AssertEqual(t, dir, root) +} diff --git a/cmd/workspace/config_test.go b/cmd/workspace/config_test.go index b6c61cb..22425bb 100644 --- a/cmd/workspace/config_test.go +++ b/cmd/workspace/config_test.go @@ -8,44 +8,43 @@ import ( func TestLoadConfig_RelativeDirFindsParentConfig_Good(t *testing.T) { root := t.TempDir() - mustNoError(t, os.MkdirAll(filepath.Join(root, ".core"), 0o755)) - mustNoError(t, os.MkdirAll(filepath.Join(root, "packages", "app"), 0o755)) - mustNoError(t, os.WriteFile(filepath.Join(root, ".core", "workspace.yaml"), []byte(`version: 1 + if err := os.MkdirAll(filepath.Join(root, ".core"), 0o755); err != nil { + t.Fatalf("create .core dir: %v", err) + } + if err := os.MkdirAll(filepath.Join(root, "packages", "app"), 0o755); err != nil { + t.Fatalf("create app dir: %v", err) + } + if err := os.WriteFile(filepath.Join(root, ".core", "workspace.yaml"), []byte(`version: 1 active: app packages_dir: ./packages -`), 0o600)) +`), 0o600); err != nil { + t.Fatalf("write workspace config: %v", err) + } originalWD, err := os.Getwd() - mustNoError(t, err) + if err != nil { + t.Fatalf("get working directory: %v", err) + } t.Cleanup(func() { - mustNoError(t, os.Chdir(originalWD)) + if err := os.Chdir(originalWD); err != nil { + t.Fatalf("restore working directory: %v", err) + } }) - mustNoError(t, os.Chdir(filepath.Join(root, "packages", "app"))) + if err := os.Chdir(filepath.Join(root, "packages", "app")); err != nil { + t.Fatalf("change working directory: %v", err) + } cfg, err := LoadConfig(".") - mustNoError(t, err) - mustNotNil(t, cfg) - mustEqual(t, "app", cfg.Active) - mustEqual(t, "./packages", cfg.PackagesDir) -} - -func mustNoError(t *testing.T, err error) { - t.Helper() if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf("load config: %v", err) } -} - -func mustEqual[T comparable](t *testing.T, want, got T) { - t.Helper() - if want != got { - t.Fatalf("want %v, got %v", want, got) + if cfg == nil { + t.Fatal("expected config") } -} - -func mustNotNil(t *testing.T, got any) { - t.Helper() - if got == nil { - t.Fatal("expected non-nil") + if cfg.Active != "app" { + t.Fatalf("active = %q, want app", cfg.Active) + } + if cfg.PackagesDir != "./packages" { + t.Fatalf("packages dir = %q, want ./packages", cfg.PackagesDir) } } diff --git a/deploy/coolify/ax7_test.go b/deploy/coolify/ax7_test.go new file mode 100644 index 0000000..fc6f285 --- /dev/null +++ b/deploy/coolify/ax7_test.go @@ -0,0 +1,582 @@ +package coolify + +import core "dappco.re/go" + +func ax7StubCoolifyInit(t *core.T, err error) { + original := initEmbeddedPython + initEmbeddedPython = func() error { return err } + t.Cleanup(func() { initEmbeddedPython = original }) +} + +func TestAX7_DefaultConfig_Good(t *core.T) { + t.Setenv("COOLIFY_URL", "https://coolify.example") + t.Setenv("COOLIFY_TOKEN", "secret") + cfg := DefaultConfig() + + core.AssertEqual(t, "https://coolify.example", cfg.BaseURL) + core.AssertEqual(t, "secret", cfg.APIToken) + core.AssertTrue(t, cfg.VerifySSL) +} + +func TestAX7_DefaultConfig_Bad(t *core.T) { + t.Setenv("COOLIFY_URL", "") + t.Setenv("COOLIFY_TOKEN", "") + cfg := DefaultConfig() + + core.AssertEqual(t, "", cfg.BaseURL) + core.AssertEqual(t, "", cfg.APIToken) + core.AssertEqual(t, 30, cfg.Timeout) +} + +func TestAX7_DefaultConfig_Ugly(t *core.T) { + t.Setenv("COOLIFY_URL", "http://localhost:8000/") + t.Setenv("COOLIFY_TOKEN", " token with spaces ") + cfg := DefaultConfig() + + core.AssertEqual(t, "http://localhost:8000/", cfg.BaseURL) + core.AssertEqual(t, " token with spaces ", cfg.APIToken) + core.AssertTrue(t, cfg.VerifySSL) +} + +func TestAX7_NewClient_Good(t *core.T) { + ax7StubCoolifyInit(t, nil) + client, err := NewClient(Config{BaseURL: "https://coolify.example", APIToken: "secret", Timeout: 5}) + + core.AssertNoError(t, err) + core.AssertEqual(t, "https://coolify.example", client.baseURL) + core.AssertEqual(t, "secret", client.apiToken) +} + +func TestAX7_NewClient_Bad(t *core.T) { + ax7StubCoolifyInit(t, nil) + client, err := NewClient(Config{APIToken: "secret"}) + + core.AssertError(t, err) + core.AssertNil(t, client) +} + +func TestAX7_NewClient_Ugly(t *core.T) { + ax7StubCoolifyInit(t, core.AnError) + client, err := NewClient(Config{BaseURL: "https://coolify.example", APIToken: "secret"}) + + core.AssertError(t, err) + core.AssertNil(t, client) +} + +func TestAX7_Client_Call_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "list-servers", operation) + core.AssertEmpty(t, params) + return map[string]any{"ok": true}, nil + }} + + result, err := client.Call(core.Background(), "list-servers", nil) + core.AssertNoError(t, err) + core.AssertEqual(t, true, result["ok"]) +} + +func TestAX7_Client_Call_Bad(t *core.T) { + var client *Client + result, err := client.Call(core.Background(), "list-servers", nil) + + core.AssertError(t, err) + core.AssertNil(t, result) +} + +func TestAX7_Client_Call_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", operation) + core.AssertEqual(t, "value", params["key"]) + return map[string]any{}, nil + }} + + result, err := client.Call(core.Background(), "", map[string]any{"key": "value"}) + core.AssertNoError(t, err) + core.AssertEmpty(t, result) +} + +func TestAX7_Client_ListServers_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "list-servers", operation) + core.AssertEmpty(t, params) + return map[string]any{"result": []any{map[string]any{"uuid": "srv-1"}}}, nil + }} + + items, err := client.ListServers(core.Background()) + core.AssertNoError(t, err) + core.AssertEqual(t, "srv-1", items[0]["uuid"]) +} + +func TestAX7_Client_ListServers_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + items, err := client.ListServers(core.Background()) + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, items) +} + +func TestAX7_Client_ListServers_Ugly(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { + return map[string]any{"result": "none"}, nil + }} + items, err := client.ListServers(core.Background()) + + core.AssertNoError(t, err) + core.AssertNil(t, items) +} + +func TestAX7_Client_GetServer_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-server-by-uuid", operation) + core.AssertEqual(t, "srv-1", params["uuid"]) + return map[string]any{"uuid": "srv-1"}, nil + }} + + item, err := client.GetServer(core.Background(), "srv-1") + core.AssertNoError(t, err) + core.AssertEqual(t, "srv-1", item["uuid"]) +} + +func TestAX7_Client_GetServer_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.GetServer(core.Background(), "srv-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_GetServer_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["uuid"]) + return map[string]any{"uuid": ""}, nil + }} + + item, err := client.GetServer(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEqual(t, "", item["uuid"]) +} + +func TestAX7_Client_ValidateServer_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "validate-server-by-uuid", operation) + core.AssertEqual(t, "srv-1", params["uuid"]) + return map[string]any{"valid": true}, nil + }} + + item, err := client.ValidateServer(core.Background(), "srv-1") + core.AssertNoError(t, err) + core.AssertEqual(t, true, item["valid"]) +} + +func TestAX7_Client_ValidateServer_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.ValidateServer(core.Background(), "srv-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_ValidateServer_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["uuid"]) + return map[string]any{"valid": false}, nil + }} + + item, err := client.ValidateServer(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEqual(t, false, item["valid"]) +} + +func TestAX7_Client_ListProjects_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "list-projects", operation) + core.AssertEmpty(t, params) + return map[string]any{"result": []any{map[string]any{"uuid": "prj-1"}}}, nil + }} + + items, err := client.ListProjects(core.Background()) + core.AssertNoError(t, err) + core.AssertEqual(t, "prj-1", items[0]["uuid"]) +} + +func TestAX7_Client_ListProjects_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + items, err := client.ListProjects(core.Background()) + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, items) +} + +func TestAX7_Client_ListProjects_Ugly(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return map[string]any{}, nil }} + items, err := client.ListProjects(core.Background()) + + core.AssertNoError(t, err) + core.AssertNil(t, items) +} + +func TestAX7_Client_GetProject_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-project-by-uuid", operation) + core.AssertEqual(t, "prj-1", params["uuid"]) + return map[string]any{"uuid": "prj-1"}, nil + }} + + item, err := client.GetProject(core.Background(), "prj-1") + core.AssertNoError(t, err) + core.AssertEqual(t, "prj-1", item["uuid"]) +} + +func TestAX7_Client_GetProject_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.GetProject(core.Background(), "prj-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_GetProject_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["uuid"]) + return map[string]any{"uuid": ""}, nil + }} + + item, err := client.GetProject(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEqual(t, "", item["uuid"]) +} + +func TestAX7_Client_CreateProject_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "create-project", operation) + core.AssertEqual(t, "agent", params["name"]) + return map[string]any{"name": "agent"}, nil + }} + + item, err := client.CreateProject(core.Background(), "agent", "desc") + core.AssertNoError(t, err) + core.AssertEqual(t, "agent", item["name"]) +} + +func TestAX7_Client_CreateProject_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.CreateProject(core.Background(), "agent", "desc") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_CreateProject_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["name"]) + core.AssertEqual(t, "", params["description"]) + return map[string]any{"name": ""}, nil + }} + + item, err := client.CreateProject(core.Background(), "", "") + core.AssertNoError(t, err) + core.AssertEqual(t, "", item["name"]) +} + +func TestAX7_Client_ListApplications_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "list-applications", operation) + core.AssertEmpty(t, params) + return map[string]any{"result": []any{map[string]any{"uuid": "app-1"}}}, nil + }} + + items, err := client.ListApplications(core.Background()) + core.AssertNoError(t, err) + core.AssertEqual(t, "app-1", items[0]["uuid"]) +} + +func TestAX7_Client_ListApplications_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + items, err := client.ListApplications(core.Background()) + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, items) +} + +func TestAX7_Client_ListApplications_Ugly(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { + return map[string]any{"result": []any{"bad"}}, nil + }} + items, err := client.ListApplications(core.Background()) + + core.AssertNoError(t, err) + core.AssertEmpty(t, items) +} + +func TestAX7_Client_GetApplication_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-application-by-uuid", operation) + core.AssertEqual(t, "app-1", params["uuid"]) + return map[string]any{"uuid": "app-1"}, nil + }} + + item, err := client.GetApplication(core.Background(), "app-1") + core.AssertNoError(t, err) + core.AssertEqual(t, "app-1", item["uuid"]) +} + +func TestAX7_Client_GetApplication_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.GetApplication(core.Background(), "app-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_GetApplication_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["uuid"]) + return map[string]any{"uuid": ""}, nil + }} + + item, err := client.GetApplication(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEqual(t, "", item["uuid"]) +} + +func TestAX7_Client_DeployApplication_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "deploy-by-tag-or-uuid", operation) + core.AssertEqual(t, "app-1", params["uuid"]) + return map[string]any{"deployment": "queued"}, nil + }} + + item, err := client.DeployApplication(core.Background(), "app-1") + core.AssertNoError(t, err) + core.AssertEqual(t, "queued", item["deployment"]) +} + +func TestAX7_Client_DeployApplication_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.DeployApplication(core.Background(), "app-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_DeployApplication_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["uuid"]) + return map[string]any{"deployment": ""}, nil + }} + + item, err := client.DeployApplication(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEqual(t, "", item["deployment"]) +} + +func TestAX7_Client_ListDatabases_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "list-databases", operation) + core.AssertEmpty(t, params) + return map[string]any{"result": []any{map[string]any{"uuid": "db-1"}}}, nil + }} + + items, err := client.ListDatabases(core.Background()) + core.AssertNoError(t, err) + core.AssertEqual(t, "db-1", items[0]["uuid"]) +} + +func TestAX7_Client_ListDatabases_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + items, err := client.ListDatabases(core.Background()) + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, items) +} + +func TestAX7_Client_ListDatabases_Ugly(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { + return map[string]any{"result": []any{}}, nil + }} + items, err := client.ListDatabases(core.Background()) + + core.AssertNoError(t, err) + core.AssertEmpty(t, items) +} + +func TestAX7_Client_GetDatabase_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-database-by-uuid", operation) + core.AssertEqual(t, "db-1", params["uuid"]) + return map[string]any{"uuid": "db-1"}, nil + }} + + item, err := client.GetDatabase(core.Background(), "db-1") + core.AssertNoError(t, err) + core.AssertEqual(t, "db-1", item["uuid"]) +} + +func TestAX7_Client_GetDatabase_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.GetDatabase(core.Background(), "db-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_GetDatabase_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["uuid"]) + return map[string]any{"uuid": ""}, nil + }} + + item, err := client.GetDatabase(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEqual(t, "", item["uuid"]) +} + +func TestAX7_Client_ListServices_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "list-services", operation) + core.AssertEmpty(t, params) + return map[string]any{"result": []any{map[string]any{"uuid": "svc-1"}}}, nil + }} + + items, err := client.ListServices(core.Background()) + core.AssertNoError(t, err) + core.AssertEqual(t, "svc-1", items[0]["uuid"]) +} + +func TestAX7_Client_ListServices_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + items, err := client.ListServices(core.Background()) + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, items) +} + +func TestAX7_Client_ListServices_Ugly(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { + return map[string]any{"result": nil}, nil + }} + items, err := client.ListServices(core.Background()) + + core.AssertNoError(t, err) + core.AssertNil(t, items) +} + +func TestAX7_Client_GetService_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-service-by-uuid", operation) + core.AssertEqual(t, "svc-1", params["uuid"]) + return map[string]any{"uuid": "svc-1"}, nil + }} + + item, err := client.GetService(core.Background(), "svc-1") + core.AssertNoError(t, err) + core.AssertEqual(t, "svc-1", item["uuid"]) +} + +func TestAX7_Client_GetService_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.GetService(core.Background(), "svc-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_GetService_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["uuid"]) + return map[string]any{"uuid": ""}, nil + }} + + item, err := client.GetService(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEqual(t, "", item["uuid"]) +} + +func TestAX7_Client_ListEnvironments_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-environments", operation) + core.AssertEqual(t, "prj-1", params["project_uuid"]) + return map[string]any{"result": []any{map[string]any{"name": "prod"}}}, nil + }} + + items, err := client.ListEnvironments(core.Background(), "prj-1") + core.AssertNoError(t, err) + core.AssertEqual(t, "prod", items[0]["name"]) +} + +func TestAX7_Client_ListEnvironments_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + items, err := client.ListEnvironments(core.Background(), "prj-1") + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, items) +} + +func TestAX7_Client_ListEnvironments_Ugly(t *core.T) { + client := &Client{call: func(_ core.Context, _ string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "", params["project_uuid"]) + return map[string]any{"result": []any{}}, nil + }} + + items, err := client.ListEnvironments(core.Background(), "") + core.AssertNoError(t, err) + core.AssertEmpty(t, items) +} + +func TestAX7_Client_GetTeam_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-current-team", operation) + core.AssertEmpty(t, params) + return map[string]any{"name": "core"}, nil + }} + + item, err := client.GetTeam(core.Background()) + core.AssertNoError(t, err) + core.AssertEqual(t, "core", item["name"]) +} + +func TestAX7_Client_GetTeam_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + item, err := client.GetTeam(core.Background()) + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, item) +} + +func TestAX7_Client_GetTeam_Ugly(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return map[string]any{}, nil }} + item, err := client.GetTeam(core.Background()) + + core.AssertNoError(t, err) + core.AssertEmpty(t, item) +} + +func TestAX7_Client_GetTeamMembers_Good(t *core.T) { + client := &Client{call: func(_ core.Context, operation string, params map[string]any) (map[string]any, error) { + core.AssertEqual(t, "get-current-team-members", operation) + core.AssertEmpty(t, params) + return map[string]any{"result": []any{map[string]any{"name": "alice"}}}, nil + }} + + items, err := client.GetTeamMembers(core.Background()) + core.AssertNoError(t, err) + core.AssertEqual(t, "alice", items[0]["name"]) +} + +func TestAX7_Client_GetTeamMembers_Bad(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { return nil, core.AnError }} + items, err := client.GetTeamMembers(core.Background()) + + core.AssertErrorIs(t, err, core.AnError) + core.AssertNil(t, items) +} + +func TestAX7_Client_GetTeamMembers_Ugly(t *core.T) { + client := &Client{call: func(core.Context, string, map[string]any) (map[string]any, error) { + return map[string]any{"result": []any{}}, nil + }} + items, err := client.GetTeamMembers(core.Background()) + + core.AssertNoError(t, err) + core.AssertEmpty(t, items) +} diff --git a/deploy/coolify/client.go b/deploy/coolify/client.go index 1b2ad4c..d58d9ff 100644 --- a/deploy/coolify/client.go +++ b/deploy/coolify/client.go @@ -17,6 +17,7 @@ type Client struct { apiToken string timeout int verifySSL bool + call func(context.Context, string, map[string]any) (map[string]any, error) mu sync.Mutex } @@ -29,6 +30,8 @@ type Config struct { VerifySSL bool } +var initEmbeddedPython = python.Init + // DefaultConfig returns default configuration from environment. func DefaultConfig() Config { return Config{ @@ -49,7 +52,7 @@ func NewClient(cfg Config) (*Client, error) { } // Initialize Python runtime - if err := python.Init(); err != nil { + if err := initEmbeddedPython(); err != nil { return nil, log.E("coolify", "failed to initialize Python", err) } @@ -63,12 +66,19 @@ func NewClient(cfg Config) (*Client, error) { // Call invokes a Coolify API operation by operationId. func (c *Client) Call(ctx context.Context, operationID string, params map[string]any) (map[string]any, error) { + if c == nil { + return nil, log.E("coolify", "client is nil", nil) + } + c.mu.Lock() defer c.mu.Unlock() if params == nil { params = map[string]any{} } + if c.call != nil { + return c.call(ctx, operationID, params) + } // Generate and run Python script script, err := python.CoolifyScript(c.baseURL, c.apiToken, operationID, params) diff --git a/deploy/python/ax7_test.go b/deploy/python/ax7_test.go new file mode 100644 index 0000000..bdfc8ad --- /dev/null +++ b/deploy/python/ax7_test.go @@ -0,0 +1,237 @@ +package python + +import ( + "os/exec" + "sync" + + . "dappco.re/go" + embedpython "github.com/kluctl/go-embed-python/python" +) + +func ax7ResetPythonHooks(t *T) { + oldEP := ep + oldInitErr := initErr + oldNew := newEmbeddedPython + oldInitRuntime := initRuntime + oldPythonCommand := pythonCommand + + once = sync.Once{} + ep = nil + initErr = nil + + t.Cleanup(func() { + once = sync.Once{} + ep = oldEP + initErr = oldInitErr + newEmbeddedPython = oldNew + initRuntime = oldInitRuntime + pythonCommand = oldPythonCommand + }) +} + +func TestAX7_Init_Good(t *T) { + ax7ResetPythonHooks(t) + newEmbeddedPython = func(string) (*embedpython.EmbeddedPython, error) { + return &embedpython.EmbeddedPython{}, nil + } + + err := Init() + AssertNoError(t, err) + AssertNotNil(t, GetPython()) +} + +func TestAX7_Init_Bad(t *T) { + ax7ResetPythonHooks(t) + newEmbeddedPython = func(string) (*embedpython.EmbeddedPython, error) { + return nil, AnError + } + + err := Init() + AssertErrorIs(t, err, AnError) + AssertNil(t, GetPython()) +} + +func TestAX7_Init_Ugly(t *T) { + ax7ResetPythonHooks(t) + calls := 0 + newEmbeddedPython = func(string) (*embedpython.EmbeddedPython, error) { + calls++ + return &embedpython.EmbeddedPython{}, nil + } + + AssertNoError(t, Init()) + AssertNoError(t, Init()) + AssertEqual(t, 1, calls) +} + +func TestAX7_GetPython_Good(t *T) { + ax7ResetPythonHooks(t) + ep = &embedpython.EmbeddedPython{} + got := GetPython() + + AssertNotNil(t, got) + AssertTrue(t, got == ep) +} + +func TestAX7_GetPython_Bad(t *T) { + ax7ResetPythonHooks(t) + got := GetPython() + + AssertNil(t, got) + AssertNil(t, ep) +} + +func TestAX7_GetPython_Ugly(t *T) { + ax7ResetPythonHooks(t) + newEmbeddedPython = func(string) (*embedpython.EmbeddedPython, error) { + return &embedpython.EmbeddedPython{}, nil + } + RequireNoError(t, Init()) + + AssertNotNil(t, GetPython()) + AssertTrue(t, GetPython() == ep) +} + +func TestAX7_RunScript_Good(t *T) { + ax7ResetPythonHooks(t) + initRuntime = func() error { return nil } + pythonCommand = func(args ...string) (*exec.Cmd, error) { + AssertLen(t, args, 1) + return exec.Command("sh", "-c", "printf script-ok"), nil + } + + out, err := RunScript(Background(), "print('ignored')") + AssertNoError(t, err) + AssertEqual(t, "script-ok", out) +} + +func TestAX7_RunScript_Bad(t *T) { + ax7ResetPythonHooks(t) + initRuntime = func() error { return AnError } + + out, err := RunScript(Background(), "print('ignored')") + AssertErrorIs(t, err, AnError) + AssertEqual(t, "", out) +} + +func TestAX7_RunScript_Ugly(t *T) { + ax7ResetPythonHooks(t) + initRuntime = func() error { return nil } + pythonCommand = func(args ...string) (*exec.Cmd, error) { + AssertLen(t, args, 2) + return exec.Command("sh", "-c", "printf script-failed >&2; exit 7"), nil + } + + out, err := RunScript(Background(), "print('ignored')", "--flag") + AssertError(t, err) + AssertEqual(t, "", out) +} + +func TestAX7_RunModule_Good(t *T) { + ax7ResetPythonHooks(t) + initRuntime = func() error { return nil } + pythonCommand = func(args ...string) (*exec.Cmd, error) { + AssertEqual(t, []string{"-m", "json.tool"}, args) + return exec.Command("sh", "-c", "printf module-ok"), nil + } + + out, err := RunModule(Background(), "json.tool") + AssertNoError(t, err) + AssertEqual(t, "module-ok", out) +} + +func TestAX7_RunModule_Bad(t *T) { + ax7ResetPythonHooks(t) + initRuntime = func() error { return AnError } + + out, err := RunModule(Background(), "json.tool") + AssertErrorIs(t, err, AnError) + AssertEqual(t, "", out) +} + +func TestAX7_RunModule_Ugly(t *T) { + ax7ResetPythonHooks(t) + initRuntime = func() error { return nil } + pythonCommand = func(args ...string) (*exec.Cmd, error) { + AssertEqual(t, []string{"-m", "missing.module", "--help"}, args) + return exec.Command("sh", "-c", "exit 9"), nil + } + + out, err := RunModule(Background(), "missing.module", "--help") + AssertError(t, err) + AssertEqual(t, "", out) +} + +func TestAX7_DevOpsPath_Good(t *T) { + t.Setenv("DEVOPS_PATH", "/tmp/devops") + path, err := DevOpsPath() + + AssertNoError(t, err) + AssertEqual(t, "/tmp/devops", path) +} + +func TestAX7_DevOpsPath_Bad(t *T) { + t.Setenv("DEVOPS_PATH", "") + path, err := DevOpsPath() + + AssertNoError(t, err) + AssertContains(t, path, "Code/DevOps") +} + +func TestAX7_DevOpsPath_Ugly(t *T) { + t.Setenv("DEVOPS_PATH", "/tmp/dev ops") + path, err := DevOpsPath() + + AssertNoError(t, err) + AssertEqual(t, "/tmp/dev ops", path) +} + +func TestAX7_CoolifyModulePath_Good(t *T) { + t.Setenv("DEVOPS_PATH", "/tmp/devops") + path, err := CoolifyModulePath() + + AssertNoError(t, err) + AssertEqual(t, "/tmp/devops/playbooks/roles/coolify/module_utils", path) +} + +func TestAX7_CoolifyModulePath_Bad(t *T) { + t.Setenv("DEVOPS_PATH", "") + path, err := CoolifyModulePath() + + AssertNoError(t, err) + AssertContains(t, path, "playbooks/roles/coolify/module_utils") +} + +func TestAX7_CoolifyModulePath_Ugly(t *T) { + t.Setenv("DEVOPS_PATH", "/tmp/dev ops") + path, err := CoolifyModulePath() + + AssertNoError(t, err) + AssertContains(t, path, "/tmp/dev ops/") +} + +func TestAX7_CoolifyScript_Good(t *T) { + t.Setenv("DEVOPS_PATH", "/tmp/devops") + script, err := CoolifyScript("https://coolify.example", "token", "list-servers", map[string]any{"limit": 1}) + + AssertNoError(t, err) + AssertContains(t, script, "list-servers") + AssertContains(t, script, "https://coolify.example") +} + +func TestAX7_CoolifyScript_Bad(t *T) { + t.Setenv("DEVOPS_PATH", "/tmp/devops") + script, err := CoolifyScript("https://coolify.example", "token", "bad", map[string]any{"bad": func() {}}) + + AssertError(t, err) + AssertEqual(t, "", script) +} + +func TestAX7_CoolifyScript_Ugly(t *T) { + t.Setenv("DEVOPS_PATH", "/tmp/devops") + script, err := CoolifyScript("", "", "", nil) + + AssertNoError(t, err) + AssertContains(t, script, "CoolifyClient") + AssertContains(t, script, "json.loads") +} diff --git a/deploy/python/python.go b/deploy/python/python.go index 7728497..af9b378 100644 --- a/deploy/python/python.go +++ b/deploy/python/python.go @@ -17,12 +17,18 @@ var ( once sync.Once ep *python.EmbeddedPython initErr error + + newEmbeddedPython = python.NewEmbeddedPython + initRuntime = Init + pythonCommand = func(args ...string) (*exec.Cmd, error) { + return ep.PythonCmd(args...) + } ) // Init initializes the embedded Python runtime. func Init() error { once.Do(func() { - ep, initErr = python.NewEmbeddedPython("core-deploy") + ep, initErr = newEmbeddedPython("core-deploy") }) return initErr } @@ -34,7 +40,7 @@ func GetPython() *python.EmbeddedPython { // RunScript runs a Python script with the given code and returns stdout. func RunScript(ctx context.Context, code string, args ...string) (string, error) { - if err := Init(); err != nil { + if err := initRuntime(); err != nil { return "", err } @@ -43,19 +49,27 @@ func RunScript(ctx context.Context, code string, args ...string) (string, error) if err != nil { return "", log.E("python", "create temp file", err) } - defer func() { _ = os.Remove(tmpFile.Name()) }() + defer func() { + if err := os.Remove(tmpFile.Name()); err != nil && !os.IsNotExist(err) { + log.Warn("failed to remove temporary Python script", "path", tmpFile.Name(), "error", err) + } + }() if _, err := tmpFile.WriteString(code); err != nil { - _ = tmpFile.Close() + if closeErr := tmpFile.Close(); closeErr != nil { + return "", log.E("python", "close script", closeErr) + } return "", log.E("python", "write script", err) } - _ = tmpFile.Close() + if err := tmpFile.Close(); err != nil { + return "", log.E("python", "close script", err) + } // Build args: script path + any additional args cmdArgs := append([]string{tmpFile.Name()}, args...) // Get the command - cmd, err := ep.PythonCmd(cmdArgs...) + cmd, err := pythonCommand(cmdArgs...) if err != nil { return "", log.E("python", "create command", err) } @@ -75,12 +89,12 @@ func RunScript(ctx context.Context, code string, args ...string) (string, error) // RunModule runs a Python module (python -m module_name). func RunModule(ctx context.Context, module string, args ...string) (string, error) { - if err := Init(); err != nil { + if err := initRuntime(); err != nil { return "", err } cmdArgs := append([]string{"-m", module}, args...) - cmd, err := ep.PythonCmd(cmdArgs...) + cmd, err := pythonCommand(cmdArgs...) if err != nil { return "", log.E("python", "create command", err) } diff --git a/devkit/ax7_test.go b/devkit/ax7_test.go new file mode 100644 index 0000000..9eb80ac --- /dev/null +++ b/devkit/ax7_test.go @@ -0,0 +1,242 @@ +package devkit + +import . "dappco.re/go" + +func TestAX7_NewCoverageStore_Good(t *T) { + path := Path(t.TempDir(), "coverage.json") + store := NewCoverageStore(path) + + AssertNotNil(t, store) + AssertEqual(t, path, store.path) +} + +func TestAX7_NewCoverageStore_Bad(t *T) { + store := NewCoverageStore("") + AssertNotNil(t, store) + + AssertEqual(t, "", store.path) + AssertError(t, store.Append(CoverageSnapshot{})) +} + +func TestAX7_NewCoverageStore_Ugly(t *T) { + path := Path(t.TempDir(), "nested", "coverage.json") + store := NewCoverageStore(path) + + AssertNotNil(t, store) + AssertContains(t, store.path, "nested") +} + +func TestAX7_CoverageStore_Append_Good(t *T) { + store := NewCoverageStore(Path(t.TempDir(), "coverage.json")) + snapshot := CoverageSnapshot{CapturedAt: UnixTime(1770000000), Total: CoveragePackage{Name: "total", Coverage: 80}} + + err := store.Append(snapshot) + AssertNoError(t, err) + AssertTrue(t, Stat(store.path).OK) +} + +func TestAX7_CoverageStore_Append_Bad(t *T) { + dir := t.TempDir() + store := NewCoverageStore(dir) + err := store.Append(CoverageSnapshot{}) + + AssertError(t, err) + AssertContains(t, err.Error(), "is a directory") +} + +func TestAX7_CoverageStore_Append_Ugly(t *T) { + store := NewCoverageStore(Path(t.TempDir(), "coverage.json")) + err := store.Append(CoverageSnapshot{}) + + AssertNoError(t, err) + AssertTrue(t, Stat(store.path).OK) +} + +func TestAX7_CoverageStore_Load_Good(t *T) { + store := NewCoverageStore(Path(t.TempDir(), "coverage.json")) + RequireNoError(t, store.Append(CoverageSnapshot{CapturedAt: UnixTime(1)})) + + snapshots, err := store.Load() + AssertNoError(t, err) + AssertLen(t, snapshots, 1) +} + +func TestAX7_CoverageStore_Load_Bad(t *T) { + path := Path(t.TempDir(), "coverage.json") + RequireTrue(t, WriteFile(path, []byte("{"), 0o600).OK) + store := NewCoverageStore(path) + + snapshots, err := store.Load() + AssertError(t, err) + AssertNil(t, snapshots) +} + +func TestAX7_CoverageStore_Load_Ugly(t *T) { + path := Path(t.TempDir(), "coverage.json") + RequireTrue(t, WriteFile(path, []byte(" \n "), 0o600).OK) + store := NewCoverageStore(path) + + snapshots, err := store.Load() + AssertNoError(t, err) + AssertNil(t, snapshots) +} + +func TestAX7_CoverageStore_Latest_Good(t *T) { + store := NewCoverageStore(Path(t.TempDir(), "coverage.json")) + RequireNoError(t, store.Append(CoverageSnapshot{CapturedAt: UnixTime(1)})) + RequireNoError(t, store.Append(CoverageSnapshot{CapturedAt: UnixTime(2)})) + + latest, err := store.Latest() + AssertNoError(t, err) + AssertTrue(t, latest.CapturedAt.Equal(UnixTime(2))) +} + +func TestAX7_CoverageStore_Latest_Bad(t *T) { + store := NewCoverageStore(Path(t.TempDir(), "coverage.json")) + latest, err := store.Latest() + + AssertError(t, err) + AssertEqual(t, CoverageSnapshot{}, latest) +} + +func TestAX7_CoverageStore_Latest_Ugly(t *T) { + store := NewCoverageStore(Path(t.TempDir(), "coverage.json")) + RequireNoError(t, store.Append(CoverageSnapshot{})) + + latest, err := store.Latest() + AssertNoError(t, err) + AssertEqual(t, CoverageSnapshot{}, latest) +} + +func TestAX7_ParseCoverProfile_Good(t *T) { + snapshot, err := ParseCoverProfile("mode: set\npkg/a.go:1.1,2.1 2 1\n") + AssertNoError(t, err) + + AssertLen(t, snapshot.Packages, 1) + AssertEqual(t, 100.0, snapshot.Total.Coverage) +} + +func TestAX7_ParseCoverProfile_Bad(t *T) { + snapshot, err := ParseCoverProfile("mode: set\nbroken line\n") + AssertError(t, err) + + AssertEqual(t, CoverageSnapshot{}, snapshot) + AssertContains(t, err.Error(), "invalid cover profile line") +} + +func TestAX7_ParseCoverProfile_Ugly(t *T) { + snapshot, err := ParseCoverProfile(" \n ") + AssertNoError(t, err) + + AssertEmpty(t, snapshot.Packages) + AssertEqual(t, 0.0, snapshot.Total.Coverage) +} + +func TestAX7_ParseCoverOutput_Good(t *T) { + snapshot, err := ParseCoverOutput("ok \tpkg/a\t0.1s\tcoverage: 75.0% of statements\n") + AssertNoError(t, err) + + AssertLen(t, snapshot.Packages, 1) + AssertEqual(t, 75.0, snapshot.Total.Coverage) +} + +func TestAX7_ParseCoverOutput_Bad(t *T) { + snapshot, err := ParseCoverOutput("no coverage here\n") + AssertNoError(t, err) + + AssertEmpty(t, snapshot.Packages) + AssertEqual(t, 0.0, snapshot.Total.Coverage) +} + +func TestAX7_ParseCoverOutput_Ugly(t *T) { + snapshot, err := ParseCoverOutput("? \tpkg/a\t0.1s\tcoverage: 0.0% of statements\n") + AssertNoError(t, err) + + AssertLen(t, snapshot.Packages, 1) + AssertEqual(t, 0.0, snapshot.Total.Coverage) +} + +func TestAX7_CompareCoverage_Good(t *T) { + previous := CoverageSnapshot{Packages: []CoveragePackage{{Name: "pkg/a", Coverage: 90}}} + current := CoverageSnapshot{Packages: []CoveragePackage{{Name: "pkg/a", Coverage: 95}}} + comparison := CompareCoverage(previous, current) + + AssertLen(t, comparison.Improvements, 1) + AssertEqual(t, 5.0, comparison.Improvements[0].Delta) +} + +func TestAX7_CompareCoverage_Bad(t *T) { + previous := CoverageSnapshot{Packages: []CoveragePackage{{Name: "pkg/a", Coverage: 90}}} + current := CoverageSnapshot{Packages: []CoveragePackage{{Name: "pkg/a", Coverage: 80}}} + comparison := CompareCoverage(previous, current) + + AssertLen(t, comparison.Regressions, 1) + AssertEqual(t, -10.0, comparison.Regressions[0].Delta) +} + +func TestAX7_CompareCoverage_Ugly(t *T) { + comparison := CompareCoverage(CoverageSnapshot{}, CoverageSnapshot{}) + AssertEmpty(t, comparison.Regressions) + + AssertEmpty(t, comparison.Improvements) + AssertEqual(t, 0.0, comparison.TotalDelta) +} + +func TestAX7_ScanSecrets_Good(t *T) { + original := scanSecretsRunner + t.Cleanup(func() { scanSecretsRunner = original }) + scanSecretsRunner = func(string) ([]byte, error) { + return []byte("RuleID,File,StartLine,StartColumn,Match\ngithub-token,config.yml,2,3,ghp_exampletoken1234567890\n"), nil + } + + findings, err := ScanSecrets("/tmp/project") + AssertNoError(t, err) + AssertEqual(t, "github-token", findings[0].Rule) +} + +func TestAX7_ScanSecrets_Bad(t *T) { + original := scanSecretsRunner + t.Cleanup(func() { scanSecretsRunner = original }) + scanSecretsRunner = func(string) ([]byte, error) { return nil, AnError } + + findings, err := ScanSecrets("/tmp/project") + AssertError(t, err) + AssertNil(t, findings) +} + +func TestAX7_ScanSecrets_Ugly(t *T) { + original := scanSecretsRunner + t.Cleanup(func() { scanSecretsRunner = original }) + scanSecretsRunner = func(string) ([]byte, error) { return nil, nil } + + findings, err := ScanSecrets("/tmp/project") + AssertNoError(t, err) + AssertNil(t, findings) +} + +func TestAX7_ScanDir_Good(t *T) { + dir := t.TempDir() + RequireTrue(t, WriteFile(Path(dir, "config.env"), []byte("API_KEY=abcdefghijk\n"), 0o600).OK) + findings, err := ScanDir(dir) + + AssertNoError(t, err) + AssertEqual(t, "generic-secret-assignment", findings[0].Rule) +} + +func TestAX7_ScanDir_Bad(t *T) { + findings, err := ScanDir(Path(t.TempDir(), "missing")) + AssertError(t, err) + + AssertNil(t, findings) + AssertContains(t, err.Error(), "no such file") +} + +func TestAX7_ScanDir_Ugly(t *T) { + dir := t.TempDir() + RequireTrue(t, MkdirAll(Path(dir, ".git"), 0o755).OK) + RequireTrue(t, WriteFile(Path(dir, ".git", "secret.env"), []byte("API_KEY=abcdefghijk\n"), 0o600).OK) + + findings, err := ScanDir(dir) + AssertNoError(t, err) + AssertEmpty(t, findings) +} diff --git a/devkit/coverage_test.go b/devkit/coverage_test.go index e7d5218..2dec93d 100644 --- a/devkit/coverage_test.go +++ b/devkit/coverage_test.go @@ -1,6 +1,7 @@ package devkit import ( + "math" "os" "path/filepath" "testing" @@ -13,29 +14,58 @@ github.com/acme/project/foo/foo.go:1.1,3.1 2 1 github.com/acme/project/foo/bar.go:1.1,4.1 3 0 github.com/acme/project/baz/baz.go:1.1,2.1 4 4 `) - mustNoError(t, err) - mustLen(t, snapshot.Packages, 2) - mustEqual(t, "github.com/acme/project/baz", snapshot.Packages[0].Name) - mustEqual(t, "github.com/acme/project/foo", snapshot.Packages[1].Name) - mustInDelta(t, 100.0, snapshot.Packages[0].Coverage, 0.0001) - mustInDelta(t, 40.0, snapshot.Packages[1].Coverage, 0.0001) - mustInDelta(t, 66.6667, snapshot.Total.Coverage, 0.0001) + if err != nil { + t.Fatalf("parse cover profile: %v", err) + } + if len(snapshot.Packages) != 2 { + t.Fatalf("packages length = %d, want 2", len(snapshot.Packages)) + } + if snapshot.Packages[0].Name != "github.com/acme/project/baz" { + t.Fatalf("packages[0].Name = %q, want github.com/acme/project/baz", snapshot.Packages[0].Name) + } + if snapshot.Packages[1].Name != "github.com/acme/project/foo" { + t.Fatalf("packages[1].Name = %q, want github.com/acme/project/foo", snapshot.Packages[1].Name) + } + for name, check := range map[string]struct { + got float64 + want float64 + }{ + "baz coverage": {got: snapshot.Packages[0].Coverage, want: 100.0}, + "foo coverage": {got: snapshot.Packages[1].Coverage, want: 40.0}, + "total coverage": {got: snapshot.Total.Coverage, want: 66.6667}, + } { + if math.Abs(check.got-check.want) > 0.0001 { + t.Fatalf("%s = %v, want %v", name, check.got, check.want) + } + } } func TestParseCoverProfile_Bad(t *testing.T) { _, err := ParseCoverProfile("mode: set\nbroken line") - mustError(t, err) + if err == nil { + t.Fatal("expected parse error") + } } func TestParseCoverOutput_Good(t *testing.T) { snapshot, err := ParseCoverOutput(`ok github.com/acme/project/foo 0.123s coverage: 75.0% of statements ok github.com/acme/project/bar 0.456s coverage: 50.0% of statements `) - mustNoError(t, err) - mustLen(t, snapshot.Packages, 2) - mustEqual(t, "github.com/acme/project/bar", snapshot.Packages[0].Name) - mustEqual(t, "github.com/acme/project/foo", snapshot.Packages[1].Name) - mustInDelta(t, 62.5, snapshot.Total.Coverage, 0.0001) + if err != nil { + t.Fatalf("parse cover output: %v", err) + } + if len(snapshot.Packages) != 2 { + t.Fatalf("packages length = %d, want 2", len(snapshot.Packages)) + } + if snapshot.Packages[0].Name != "github.com/acme/project/bar" { + t.Fatalf("packages[0].Name = %q, want github.com/acme/project/bar", snapshot.Packages[0].Name) + } + if snapshot.Packages[1].Name != "github.com/acme/project/foo" { + t.Fatalf("packages[1].Name = %q, want github.com/acme/project/foo", snapshot.Packages[1].Name) + } + if math.Abs(snapshot.Total.Coverage-62.5) > 0.0001 { + t.Fatalf("total coverage = %v, want 62.5", snapshot.Total.Coverage) + } } func TestCompareCoverage_Good(t *testing.T) { @@ -56,14 +86,30 @@ func TestCompareCoverage_Good(t *testing.T) { } comparison := CompareCoverage(previous, current) - mustLen(t, comparison.Regressions, 1) - mustLen(t, comparison.Improvements, 1) - mustLen(t, comparison.NewPackages, 1) - mustEmpty(t, comparison.Removed) - mustEqual(t, "pkg/a", comparison.Regressions[0].Name) - mustEqual(t, "pkg/b", comparison.Improvements[0].Name) - mustEqual(t, "pkg/c", comparison.NewPackages[0].Name) - mustInDelta(t, 4.0, comparison.TotalDelta, 0.0001) + if len(comparison.Regressions) != 1 { + t.Fatalf("regressions length = %d, want 1", len(comparison.Regressions)) + } + if len(comparison.Improvements) != 1 { + t.Fatalf("improvements length = %d, want 1", len(comparison.Improvements)) + } + if len(comparison.NewPackages) != 1 { + t.Fatalf("new packages length = %d, want 1", len(comparison.NewPackages)) + } + if len(comparison.Removed) != 0 { + t.Fatalf("removed length = %d, want 0", len(comparison.Removed)) + } + if comparison.Regressions[0].Name != "pkg/a" { + t.Fatalf("regressions[0].Name = %q, want pkg/a", comparison.Regressions[0].Name) + } + if comparison.Improvements[0].Name != "pkg/b" { + t.Fatalf("improvements[0].Name = %q, want pkg/b", comparison.Improvements[0].Name) + } + if comparison.NewPackages[0].Name != "pkg/c" { + t.Fatalf("newPackages[0].Name = %q, want pkg/c", comparison.NewPackages[0].Name) + } + if math.Abs(comparison.TotalDelta-4.0) > 0.0001 { + t.Fatalf("total delta = %v, want 4.0", comparison.TotalDelta) + } } func TestCoverageStore_Good(t *testing.T) { @@ -81,26 +127,46 @@ func TestCoverageStore_Good(t *testing.T) { Total: CoveragePackage{Name: "total", Coverage: 82.5}, } - mustNoError(t, store.Append(first)) - mustNoError(t, store.Append(second)) + if err := store.Append(first); err != nil { + t.Fatalf("append first snapshot: %v", err) + } + if err := store.Append(second); err != nil { + t.Fatalf("append second snapshot: %v", err) + } snapshots, err := store.Load() - mustNoError(t, err) - mustLen(t, snapshots, 2) - mustEqual(t, first.CapturedAt, snapshots[0].CapturedAt) - mustEqual(t, second.CapturedAt, snapshots[1].CapturedAt) + if err != nil { + t.Fatalf("load snapshots: %v", err) + } + if len(snapshots) != 2 { + t.Fatalf("snapshots length = %d, want 2", len(snapshots)) + } + if !snapshots[0].CapturedAt.Equal(first.CapturedAt) { + t.Fatalf("snapshots[0].CapturedAt = %v, want %v", snapshots[0].CapturedAt, first.CapturedAt) + } + if !snapshots[1].CapturedAt.Equal(second.CapturedAt) { + t.Fatalf("snapshots[1].CapturedAt = %v, want %v", snapshots[1].CapturedAt, second.CapturedAt) + } latest, err := store.Latest() - mustNoError(t, err) - mustEqual(t, second.CapturedAt, latest.CapturedAt) + if err != nil { + t.Fatalf("load latest snapshot: %v", err) + } + if !latest.CapturedAt.Equal(second.CapturedAt) { + t.Fatalf("latest.CapturedAt = %v, want %v", latest.CapturedAt, second.CapturedAt) + } } func TestCoverageStore_Bad(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "coverage.json") - mustNoError(t, os.WriteFile(path, []byte("{"), 0o600)) + if err := os.WriteFile(path, []byte("{"), 0o600); err != nil { + t.Fatalf("write coverage store: %v", err) + } store := NewCoverageStore(path) _, err := store.Load() - mustError(t, err) + if err == nil { + t.Fatal("expected load error") + } } diff --git a/devkit/scan_secrets_test.go b/devkit/scan_secrets_test.go index 51f4c99..d83af98 100644 --- a/devkit/scan_secrets_test.go +++ b/devkit/scan_secrets_test.go @@ -12,7 +12,9 @@ func TestScanSecrets_Good(t *testing.T) { }) scanSecretsRunner = func(dir string) ([]byte, error) { - mustEqual(t, "/tmp/project", dir) + if dir != "/tmp/project" { + t.Fatalf("dir = %q, want /tmp/project", dir) + } return []byte(`RuleID,File,StartLine,StartColumn,Description,Match github-token,config.yml,12,4,GitHub token detected,ghp_exampletoken1234567890 aws-access-key-id,creds.txt,7,1,AWS access key detected,AKIA1234567890ABCDEF @@ -20,20 +22,44 @@ aws-access-key-id,creds.txt,7,1,AWS access key detected,AKIA1234567890ABCDEF } findings, err := ScanSecrets("/tmp/project") - mustNoError(t, err) - mustLen(t, findings, 2) - - mustEqual(t, "github-token", findings[0].Rule) - mustEqual(t, "config.yml", findings[0].Path) - mustEqual(t, 12, findings[0].Line) - mustEqual(t, 4, findings[0].Column) - mustEqual(t, "ghp_exampletoken1234567890", findings[0].Snippet) - - mustEqual(t, "aws-access-key-id", findings[1].Rule) - mustEqual(t, "creds.txt", findings[1].Path) - mustEqual(t, 7, findings[1].Line) - mustEqual(t, 1, findings[1].Column) - mustEqual(t, "AKIA1234567890ABCDEF", findings[1].Snippet) + if err != nil { + t.Fatalf("scan secrets: %v", err) + } + if len(findings) != 2 { + t.Fatalf("findings length = %d, want 2", len(findings)) + } + + if findings[0].Rule != "github-token" { + t.Fatalf("findings[0].Rule = %q, want github-token", findings[0].Rule) + } + if findings[0].Path != "config.yml" { + t.Fatalf("findings[0].Path = %q, want config.yml", findings[0].Path) + } + if findings[0].Line != 12 { + t.Fatalf("findings[0].Line = %d, want 12", findings[0].Line) + } + if findings[0].Column != 4 { + t.Fatalf("findings[0].Column = %d, want 4", findings[0].Column) + } + if findings[0].Snippet != "ghp_exampletoken1234567890" { + t.Fatalf("findings[0].Snippet = %q, want ghp_exampletoken1234567890", findings[0].Snippet) + } + + if findings[1].Rule != "aws-access-key-id" { + t.Fatalf("findings[1].Rule = %q, want aws-access-key-id", findings[1].Rule) + } + if findings[1].Path != "creds.txt" { + t.Fatalf("findings[1].Path = %q, want creds.txt", findings[1].Path) + } + if findings[1].Line != 7 { + t.Fatalf("findings[1].Line = %d, want 7", findings[1].Line) + } + if findings[1].Column != 1 { + t.Fatalf("findings[1].Column = %d, want 1", findings[1].Column) + } + if findings[1].Snippet != "AKIA1234567890ABCDEF" { + t.Fatalf("findings[1].Snippet = %q, want AKIA1234567890ABCDEF", findings[1].Snippet) + } } func TestScanSecrets_ReportsFindingsOnExitError_Good(t *testing.T) { @@ -49,14 +75,26 @@ token,test.txt,3,2,Token detected,secret-value } findings, err := ScanSecrets("/tmp/project") - mustNoError(t, err) - mustLen(t, findings, 1) - mustEqual(t, "token", findings[0].Rule) - mustEqual(t, 3, findings[0].Line) - mustEqual(t, 2, findings[0].Column) + if err != nil { + t.Fatalf("scan secrets: %v", err) + } + if len(findings) != 1 { + t.Fatalf("findings length = %d, want 1", len(findings)) + } + if findings[0].Rule != "token" { + t.Fatalf("findings[0].Rule = %q, want token", findings[0].Rule) + } + if findings[0].Line != 3 { + t.Fatalf("findings[0].Line = %d, want 3", findings[0].Line) + } + if findings[0].Column != 2 { + t.Fatalf("findings[0].Column = %d, want 2", findings[0].Column) + } } func TestParseGitleaksCSV_Bad(t *testing.T) { _, err := parseGitleaksCSV([]byte("rule_id,file,start_line\nunterminated,\"broken")) - mustError(t, err) + if err == nil { + t.Fatal("expected parse error") + } } diff --git a/devkit/secret_test.go b/devkit/secret_test.go index 44eec68..2e5af25 100644 --- a/devkit/secret_test.go +++ b/devkit/secret_test.go @@ -9,47 +9,91 @@ import ( func TestScanDir_Good(t *testing.T) { root := t.TempDir() - mustNoError(t, os.WriteFile(filepath.Join(root, "config.yml"), []byte(` + if err := os.WriteFile(filepath.Join(root, "config.yml"), []byte(` api_key: "ghp_abcdefghijklmnopqrstuvwxyz1234" -`), 0o600)) +`), 0o600); err != nil { + t.Fatalf("write config.yml: %v", err) + } - mustNoError(t, os.Mkdir(filepath.Join(root, "nested"), 0o755)) - mustNoError(t, os.WriteFile(filepath.Join(root, "nested", "creds.txt"), []byte("access_key = AKIA1234567890ABCDEF\n"), 0o600)) + if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil { + t.Fatalf("create nested dir: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "nested", "creds.txt"), []byte("access_key = AKIA1234567890ABCDEF\n"), 0o600); err != nil { + t.Fatalf("write creds.txt: %v", err) + } findings, err := ScanDir(root) - mustNoError(t, err) - mustLen(t, findings, 2) + if err != nil { + t.Fatalf("scan dir: %v", err) + } + if len(findings) != 2 { + t.Fatalf("findings length = %d, want 2", len(findings)) + } - mustEqual(t, "github-token", findings[0].Rule) - mustEqual(t, 2, findings[0].Line) - mustEqual(t, "config.yml", filepath.Base(findings[0].Path)) + if findings[0].Rule != "github-token" { + t.Fatalf("findings[0].Rule = %q, want %q", findings[0].Rule, "github-token") + } + if findings[0].Line != 2 { + t.Fatalf("findings[0].Line = %d, want 2", findings[0].Line) + } + if got := filepath.Base(findings[0].Path); got != "config.yml" { + t.Fatalf("findings[0] path base = %q, want %q", got, "config.yml") + } - mustEqual(t, "aws-access-key-id", findings[1].Rule) - mustEqual(t, 1, findings[1].Line) - mustEqual(t, "creds.txt", filepath.Base(findings[1].Path)) + if findings[1].Rule != "aws-access-key-id" { + t.Fatalf("findings[1].Rule = %q, want %q", findings[1].Rule, "aws-access-key-id") + } + if findings[1].Line != 1 { + t.Fatalf("findings[1].Line = %d, want 1", findings[1].Line) + } + if got := filepath.Base(findings[1].Path); got != "creds.txt" { + t.Fatalf("findings[1] path base = %q, want %q", got, "creds.txt") + } } func TestScanDir_SkipsBinaryAndIgnoredDirs_Good(t *testing.T) { root := t.TempDir() - mustNoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755)) - mustNoError(t, os.WriteFile(filepath.Join(root, ".git", "config"), []byte("token=ghp_abcdefghijklmnopqrstuvwxyz1234"), 0o600)) - mustNoError(t, os.WriteFile(filepath.Join(root, "blob.bin"), []byte{0, 1, 2, 3, 4}, 0o600)) + if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil { + t.Fatalf("create .git dir: %v", err) + } + if err := os.WriteFile(filepath.Join(root, ".git", "config"), []byte("token=ghp_abcdefghijklmnopqrstuvwxyz1234"), 0o600); err != nil { + t.Fatalf("write .git config: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "blob.bin"), []byte{0, 1, 2, 3, 4}, 0o600); err != nil { + t.Fatalf("write blob.bin: %v", err) + } findings, err := ScanDir(root) - mustNoError(t, err) - mustEmpty(t, findings) + if err != nil { + t.Fatalf("scan dir: %v", err) + } + if len(findings) != 0 { + t.Fatalf("findings length = %d, want 0", len(findings)) + } } func TestScanDir_ReportsGenericAssignments_Bad(t *testing.T) { root := t.TempDir() - mustNoError(t, os.WriteFile(filepath.Join(root, "secrets.env"), []byte("client_secret: abcdefghijklmnop\n"), 0o600)) + if err := os.WriteFile(filepath.Join(root, "secrets.env"), []byte("client_secret: abcdefghijklmnop\n"), 0o600); err != nil { + t.Fatalf("write secrets.env: %v", err) + } findings, err := ScanDir(root) - mustNoError(t, err) - mustLen(t, findings, 1) - mustEqual(t, "generic-secret-assignment", findings[0].Rule) - mustEqual(t, 1, findings[0].Line) - mustEqual(t, 1, findings[0].Column) + if err != nil { + t.Fatalf("scan dir: %v", err) + } + if len(findings) != 1 { + t.Fatalf("findings length = %d, want 1", len(findings)) + } + if findings[0].Rule != "generic-secret-assignment" { + t.Fatalf("findings[0].Rule = %q, want %q", findings[0].Rule, "generic-secret-assignment") + } + if findings[0].Line != 1 { + t.Fatalf("findings[0].Line = %d, want 1", findings[0].Line) + } + if findings[0].Column != 1 { + t.Fatalf("findings[0].Column = %d, want 1", findings[0].Column) + } } diff --git a/devkit/test_helpers_test.go b/devkit/test_helpers_test.go deleted file mode 100644 index 0dba87d..0000000 --- a/devkit/test_helpers_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package devkit - -import ( - "math" - "testing" -) - -func mustNoError(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func mustError(t *testing.T, err error) { - t.Helper() - if err == nil { - t.Fatal("expected error, got nil") - } -} - -func mustEqual[T comparable](t *testing.T, want, got T) { - t.Helper() - if want != got { - t.Fatalf("want %v, got %v", want, got) - } -} - -func mustLen[T any](t *testing.T, got []T, want int) { - t.Helper() - if len(got) != want { - t.Fatalf("want length %d, got %d", want, len(got)) - } -} - -func mustEmpty[T any](t *testing.T, got []T) { - t.Helper() - if len(got) != 0 { - t.Fatalf("expected empty, got %d entries", len(got)) - } -} - -func mustInDelta(t *testing.T, want, got, delta float64) { - t.Helper() - if math.Abs(want-got) > delta { - t.Fatalf("want %v±%v, got %v", want, delta, got) - } -} diff --git a/docs/architecture.md b/docs/architecture.md index b90aee8..6f1ff41 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -469,7 +469,6 @@ Each generator implements the `Generator` interface. Supported languages: TypeSc | `github.com/spf13/cobra` | CLI framework for `build/buildcmd/` | | `golang.org/x/crypto` | SSH connections in `ansible/ssh.go` | | `gopkg.in/yaml.v3` | Playbook and config YAML parsing | -| `github.com/stretchr/testify` | Test assertions | ## Dependency on `forge.lthn.ai/core/go` diff --git a/docs/development.md b/docs/development.md index cfbae77..a7423e6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -85,21 +85,23 @@ func TestParsePlaybook_Bad(t *testing.T) { ... } func TestParsePlaybook_Ugly(t *testing.T) { ... } ``` -### Assertion Library +### Assertions -Use `github.com/stretchr/testify`. Prefer `require` over `assert` when subsequent assertions depend on the previous one passing: +Use the standard `testing` package. Do not add third-party assertion libraries: ```go import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestSomething_Good(t *testing.T) { result, err := SomeFunction() - require.NoError(t, err) - assert.Equal(t, "expected", result.Field) + if err != nil { + t.Fatalf("some function: %v", err) + } + if result.Field != "expected" { + t.Fatalf("field = %q, want %q", result.Field, "expected") + } } ``` diff --git a/go.mod b/go.mod index 7128d69..8f56d68 100644 --- a/go.mod +++ b/go.mod @@ -3,64 +3,98 @@ module dappco.re/go/devops go 1.26.0 require ( - code.gitea.io/sdk/gitea v0.23.2 // Note: Gitea SDK for repository and automation API integration; no core.* equivalent. + code.gitea.io/sdk/gitea v0.24.1 // Note: Gitea SDK for repository and automation API integration; no core.* equivalent. dappco.re/go/agent v0.8.0-alpha.1 - dappco.re/go/core v0.8.0-alpha.1 - dappco.re/go/cli v0.8.0-alpha.1 dappco.re/go/container v0.8.0-alpha.1 dappco.re/go/i18n v0.8.0-alpha.1 dappco.re/go/io v0.8.0-alpha.1 dappco.re/go/log v0.8.0-alpha.1 dappco.re/go/scm v0.8.0-alpha.1 github.com/kluctl/go-embed-python v0.0.0-3.13.1-20241219-1 // Note: CPython embedding for Ansible playbook execution; no go/* equivalent. - golang.org/x/term v0.41.0 - golang.org/x/text v0.35.0 + golang.org/x/term v0.42.0 + golang.org/x/text v0.36.0 gopkg.in/yaml.v3 v3.0.1 // Note: YAML parser for Ansible inventory and playbook files; no core.* YAML equivalent. ) require ( - codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 // indirect + codeberg.org/forgejo/go-sdk v0.0.0 // indirect + dappco.re/go v0.9.0 + dappco.re/go/cli v0.8.0-alpha.1 dappco.re/go/config v0.8.0-alpha.1 // indirect dappco.re/go/inference v0.8.0-alpha.1 // indirect - github.com/42wim/httpsig v1.2.3 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/42wim/httpsig v1.2.4 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/goccy/go-json v0.10.6 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.49.0 // indirect + golang.org/x/crypto v0.50.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/sys v0.43.0 // indirect ) + +require ( + dappco.re/go/core v0.8.0-alpha.1 // indirect + dappco.re/go/core/i18n v0.2.3 // indirect + dappco.re/go/core/inference v0.2.1 // indirect + dappco.re/go/core/log v0.1.2 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect +) + +replace ( + codeberg.org/forgejo/go-sdk => github.com/dAppCore/go-scm/third_party/forgejo v0.0.0-20260424224729-c5374e1b928e + dappco.re/go/agent => github.com/dAppCore/agent v0.8.0-alpha.1 + dappco.re/go/ai => github.com/dAppCore/go-ai v0.8.0-alpha.1 + dappco.re/go/api => github.com/dAppCore/api v0.8.0-alpha.1 + dappco.re/go/config => ../go-config + dappco.re/go/container => github.com/dAppCore/go-container v0.8.0-alpha.1 + dappco.re/go/forge => github.com/dAppCore/go-forge v0.8.0-alpha.1 + dappco.re/go/i18n => github.com/dAppCore/go-i18n v0.8.0-alpha.1 + dappco.re/go/inference => github.com/dAppCore/go-inference v0.8.0-alpha.1 + dappco.re/go/io => github.com/dAppCore/go-io v0.8.0-alpha.1 + dappco.re/go/log => github.com/dAppCore/go-log v0.8.0-alpha.1 + dappco.re/go/mcp => github.com/dAppCore/mcp v0.8.0-alpha.1 + dappco.re/go/process => github.com/dAppCore/go-process v0.8.0-alpha.1 + dappco.re/go/rag => github.com/dAppCore/go-rag v0.8.0-alpha.1 + dappco.re/go/scm => github.com/dAppCore/go-scm v0.8.0-alpha.1 + dappco.re/go/store => github.com/dAppCore/go-store v0.8.0-alpha.1 + dappco.re/go/webview => github.com/dAppCore/go-webview v0.8.0-alpha.1 + dappco.re/go/ws => github.com/dAppCore/go-ws v0.8.0-alpha.1 +) + +replace dappco.re/go => ../go + +replace dappco.re/go/cli => dappco.re/go/core/cli v0.5.2 diff --git a/go.sum b/go.sum index c281c45..a6ba9dd 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,17 @@ -code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= -code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= -codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI= -codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs= -dappco.re/go/agent v0.8.0-alpha.1 h1:7jtrDGh5CHUVsvvQiG8gjQxfdlI+ncJrIHXEMksJ8bc= -dappco.re/go/agent v0.8.0-alpha.1/go.mod h1:jiShGsIfHS7b7rJXMdb30K+wKL8Kx8w/VUrLNDYRbCo= -dappco.re/go/agent v0.11.0 h1:5PKzxJf+z0WF+QsxgkMwvDUODj38DGCx0uMk1KxtWkg= -dappco.re/go/agent v0.11.0/go.mod h1:nBF4HMMSZD/YJg+MTHqTv71csgFlCyy62Ux084yjw+U= -dappco.re/go/cli v0.8.0-alpha.1 h1:UUnkSvAgNeRtu4kc96hr4WUpe9WTBxDY+1Co5IDVlbk= -dappco.re/go/cli v0.8.0-alpha.1/go.mod h1:jRJuSyEB7pAmyiAyTPSh7l1ens627vfxhBcUhi3sOEY= -dappco.re/go/config v0.8.0-alpha.1 h1:YpfPi7PHId0Wc2C/h07rmTZG06a+ONHrBLG9KDg45Uo= -dappco.re/go/config v0.8.0-alpha.1/go.mod h1:Ryvf7Fncq4p+mZQnHjP5h8OmDcbE2JBf99E6hDdpeN4= -dappco.re/go/container v0.8.0-alpha.1 h1:jrC308wXpooaHMjvhEvPwPfK4KOXTuFYz4y/Es+uhY4= -dappco.re/go/container v0.8.0-alpha.1/go.mod h1:5F+NPSBG3LtgfBTGvmGcVWLmax4LrmxBgexOHG4gnKc= +code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8= +code.gitea.io/sdk/gitea v0.24.1/go.mod h1:5/77BL3sHneCMEiZaMT9lfTvnnibsYxyO48mceCF3qA= dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= dappco.re/go/core/cli v0.5.2 h1:mo+PERo3lUytE+r3ArHr8o2nTftXjgPPsU/rn3ETXDM= dappco.re/go/core/cli v0.5.2/go.mod h1:D4zfn3ec/hb72AWX/JWDvkW+h2WDKQcxGUrzoss7q2s= -dappco.re/go/core/config v0.2.3 h1:sEX0Vtm3WfzsJWhqegWTVr2W90MK3BQ6FQ3cU/2MC+o= -dappco.re/go/core/config v0.2.3/go.mod h1:/rOblY96zfANXywS+WCukJYESmmkeFnYWvI19vy5NYY= -dappco.re/go/core/container v0.2.2 h1:x4JI/GmtX/TGBGa7WJu6dSVQzAHpptKHtAKvwYUxAyg= -dappco.re/go/core/container v0.2.2/go.mod h1:XEV22GjJa8zQhawKnt8dDFEQ1NqSy7xG/bwAd8iqcDA= dappco.re/go/core/i18n v0.2.3 h1:GqFaTR1I0SfSEc4WtsAkgao+jp8X5qcMPqrX0eMAOrY= dappco.re/go/core/i18n v0.2.3/go.mod h1:LoyX/4fIEJO/wiHY3Q682+4P0Ob7zPemcATfwp0JBUg= -dappco.re/go/core/inference v0.3.0 h1:ANFnlVO1LEYDipeDeBgqmb8CHvOTUFhMPyfyHGqO0IY= -dappco.re/go/core/inference v0.3.0/go.mod h1:wbRY0v6iwOoJCpTvcBFarAM08bMgpPcrF6yv3vccYoA= -dappco.re/go/core/io v0.4.1 h1:15dm7ldhFIAuZOrBiQG6XVZDpSvCxtZsUXApwTAB3wQ= -dappco.re/go/core/io v0.4.1/go.mod h1:w71dukyunczLb8frT9JOd5B78PjwWQD3YAXiCt3AcPA= +dappco.re/go/core/inference v0.2.1 h1:/kgZuG4tXibgwTB3VIB1zg9UMCHRm9T3EWoDLjptBpY= +dappco.re/go/core/inference v0.2.1/go.mod h1:wbRY0v6iwOoJCpTvcBFarAM08bMgpPcrF6yv3vccYoA= dappco.re/go/core/log v0.1.2 h1:pQSZxKD8VycdvjNJmatXbPSq2OxcP2xHbF20zgFIiZI= dappco.re/go/core/log v0.1.2/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= -dappco.re/go/core/scm v0.6.1 h1:nQWr2AGreLzhp//2zZolol87TCKlzV2/I/hpBVkv0Gc= -dappco.re/go/core/scm v0.6.1/go.mod h1:fYy/xazjyv84X8sxBIpTBikSdU5nQq4qf/IR2hXnd5E= -dappco.re/go/i18n v0.8.0-alpha.1 h1:9LI/PrF41XeQu69eOaBTz3LMrXTJ08O2f1EEATq9k5A= -dappco.re/go/i18n v0.8.0-alpha.1/go.mod h1:aSfWSAW2EVh/aMbMplc27URnjl6DvRVvWfvRC2my7AY= -dappco.re/go/inference v0.8.0-alpha.1 h1:Cc3YZr04rNSqqHQBm7v53mzfn6e17sf7oDe+TqQnzwo= -dappco.re/go/inference v0.8.0-alpha.1/go.mod h1:vMXtaGSKvom7B5rjOjzl4taSOXbbVmnsLlYd0X/PFo0= -dappco.re/go/io v0.8.0-alpha.1 h1:tIJ/Nd6lGr2DFEUj2HzGM8dPglS5bEAI4h2RAgzGCNE= -dappco.re/go/io v0.8.0-alpha.1/go.mod h1:5u1TImtXPdJKDgh59Nw4rsbMUkq02uVDDsL5bE1mhBk= -dappco.re/go/log v0.8.0-alpha.1 h1:eXTdrt88Ovbdm0KJkJDaEpgLUHUZgJ2xYEu2uN3eV4I= -dappco.re/go/log v0.8.0-alpha.1/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c= -dappco.re/go/scm v0.8.0-alpha.1 h1:pXiO5Hp5tky3shekYERUK9KsQy9xoWQQW0I40mPyKvA= -dappco.re/go/scm v0.8.0-alpha.1/go.mod h1:11xL67SU5TJ+fTBLyqYDDwotl7Y1qy5rWY+JgEQ16UQ= -github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= -github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= +github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= +github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -61,6 +31,23 @@ github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3 github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/dAppCore/agent v0.8.0-alpha.1 h1:ubnM4dh7TabJBT84xXkQBIzUi8yu3qQmX/0CVrtyf4M= +github.com/dAppCore/agent v0.8.0-alpha.1/go.mod h1:jiShGsIfHS7b7rJXMdb30K+wKL8Kx8w/VUrLNDYRbCo= +github.com/dAppCore/go-container v0.8.0-alpha.1 h1:kmJ2UAyEPWcnWcJ3ee9xDlfEeNsrSh9bcQvl+aZGEj0= +github.com/dAppCore/go-container v0.8.0-alpha.1/go.mod h1:mUhwLQuvvplHkThNauwB/Yxq7tM7ZLBwx/fIrk7yeO4= +github.com/dAppCore/go-i18n v0.8.0-alpha.1 h1:jZ+neNdWR3LdDtukoNnGWeTynoLZR12UC/gprQM7D9I= +github.com/dAppCore/go-i18n v0.8.0-alpha.1/go.mod h1:aSfWSAW2EVh/aMbMplc27URnjl6DvRVvWfvRC2my7AY= +github.com/dAppCore/go-inference v0.8.0-alpha.1 h1:feRcT6vRelon8j1tQfg2ZrD7Y3vLjydVeHMbeVlyxJ0= +github.com/dAppCore/go-inference v0.8.0-alpha.1/go.mod h1:rfNXLcfMilEI3nKpcdrC0PQKyUyaf6bDYseowgRwDP8= +github.com/dAppCore/go-io v0.8.0-alpha.1 h1:DWmaksBY7FpwoQnRHYDN/yPCGLMR794D8OcNdk0RyB8= +github.com/dAppCore/go-io v0.8.0-alpha.1/go.mod h1:491Lt0LOTK4/88EGWVWhrACuXAoxPXvXYu/iIwYc9C0= +github.com/dAppCore/go-log v0.8.0-alpha.1 h1:yAhNK38/QHBaxXgz0Db5HUkrgX16SkxbsVYJ1DvsQn4= +github.com/dAppCore/go-log v0.8.0-alpha.1/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c= +github.com/dAppCore/go-scm v0.8.0-alpha.1 h1:jMo3A2MN1xtybf7Z/kNbdBXH4D4VJIJoO+vxdqUuCOM= +github.com/dAppCore/go-scm v0.8.0-alpha.1/go.mod h1:11xL67SU5TJ+fTBLyqYDDwotl7Y1qy5rWY+JgEQ16UQ= +github.com/dAppCore/go-scm/third_party/forgejo v0.0.0-20260424224729-c5374e1b928e h1:EWNlwllH8MSI1Zv++EWX03I5fOcLOrn/7L0FK7m40lA= +github.com/dAppCore/go-scm/third_party/forgejo v0.0.0-20260424224729-c5374e1b928e/go.mod h1:uG/dXIqfx0NfIjTyTwdB38VbGfPlf2fueeEP3nvSPrg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= @@ -77,12 +64,10 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= -github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -109,6 +94,7 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -131,10 +117,18 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -142,8 +136,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -155,15 +149,15 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/snapshot/ax7_test.go b/snapshot/ax7_test.go new file mode 100644 index 0000000..dea8a7d --- /dev/null +++ b/snapshot/ax7_test.go @@ -0,0 +1,66 @@ +package snapshot + +import ( + . "dappco.re/go" + "dappco.re/go/scm/manifest" +) + +func ax7Manifest() *manifest.Manifest { + return &manifest.Manifest{ + Code: "agent", + Name: "Agent", + Version: "1.2.3", + Description: "test manifest", + Modules: []string{"core/devops"}, + } +} + +func TestAX7_Generate_Good(t *T) { + data, err := Generate(ax7Manifest(), "abc123", "v1.2.3") + AssertNoError(t, err) + AssertContains(t, string(data), `"code": "agent"`) + AssertContains(t, string(data), `"tag": "v1.2.3"`) +} + +func TestAX7_Generate_Bad(t *T) { + data, err := Generate(nil, "abc123", "v1.2.3") + AssertError(t, err) + + AssertNil(t, data) + AssertContains(t, err.Error(), "manifest is nil") +} + +func TestAX7_Generate_Ugly(t *T) { + data, err := Generate(&manifest.Manifest{}, "", "") + AssertNoError(t, err) + + AssertContains(t, string(data), `"schema": 1`) + AssertContains(t, string(data), `"built":`) +} + +func TestAX7_GenerateAt_Good(t *T) { + built := UnixTime(1770000000).UTC() + data, err := GenerateAt(ax7Manifest(), "deadbeef", "v1.2.3", built) + AssertNoError(t, err) + + AssertContains(t, string(data), `"commit": "deadbeef"`) + AssertContains(t, string(data), TimeFormat(built, TimeRFC3339)) +} + +func TestAX7_GenerateAt_Bad(t *T) { + data, err := GenerateAt(nil, "deadbeef", "v1.2.3", UnixTime(0)) + AssertError(t, err) + + AssertNil(t, data) + AssertContains(t, err.Error(), "manifest is nil") +} + +func TestAX7_GenerateAt_Ugly(t *T) { + m := ax7Manifest() + m.Permissions.Read = []string{"."} + data, err := GenerateAt(m, "", "", UnixTime(-1).UTC()) + + AssertNoError(t, err) + AssertContains(t, string(data), `"permissions"`) + AssertContains(t, string(data), `1969-12-31T23:59:59Z`) +} diff --git a/snapshot/snapshot_test.go b/snapshot/snapshot_test.go index 92b3a83..9fa2ab8 100644 --- a/snapshot/snapshot_test.go +++ b/snapshot/snapshot_test.go @@ -2,6 +2,8 @@ package snapshot import ( "encoding/json" + "slices" + "strings" "testing" "time" @@ -28,28 +30,51 @@ func TestGenerate_Good(t *testing.T) { } data, err := GenerateAt(m, "abc123def456", "v1.0.0", fixedTime) - mustNoError(t, err) + if err != nil { + t.Fatalf("generate snapshot: %v", err) + } var snap Snapshot - mustNoError(t, json.Unmarshal(data, &snap)) + if err := json.Unmarshal(data, &snap); err != nil { + t.Fatalf("unmarshal snapshot: %v", err) + } - mustEqual(t, 1, snap.Schema) - mustEqual(t, "test-app", snap.Code) - mustEqual(t, "Test App", snap.Name) - mustEqual(t, "1.0.0", snap.Version) - mustEqual(t, "A test application", snap.Description) - mustEqual(t, "abc123def456", snap.Commit) - mustEqual(t, "v1.0.0", snap.Tag) - mustEqual(t, "2026-03-09T15:00:00Z", snap.Built) - mustEqual(t, "HLCRF", snap.Layout) - mustEqual(t, "main-content", snap.Slots["C"]) - mustLenMap(t, snap.Daemons, 1) - mustEqual(t, "core-php", snap.Daemons["serve"].Binary) + if snap.Schema != 1 { + t.Fatalf("schema = %d, want 1", snap.Schema) + } + for name, check := range map[string]struct { + got string + want string + }{ + "code": {got: snap.Code, want: "test-app"}, + "name": {got: snap.Name, want: "Test App"}, + "version": {got: snap.Version, want: "1.0.0"}, + "description": {got: snap.Description, want: "A test application"}, + "commit": {got: snap.Commit, want: "abc123def456"}, + "tag": {got: snap.Tag, want: "v1.0.0"}, + "built": {got: snap.Built, want: "2026-03-09T15:00:00Z"}, + "layout": {got: snap.Layout, want: "HLCRF"}, + "slot C": {got: snap.Slots["C"], want: "main-content"}, + } { + if check.got != check.want { + t.Fatalf("%s = %q, want %q", name, check.got, check.want) + } + } + if len(snap.Daemons) != 1 { + t.Fatalf("daemons length = %d, want 1", len(snap.Daemons)) + } + if snap.Daemons["serve"].Binary != "core-php" { + t.Fatalf("serve binary = %q, want core-php", snap.Daemons["serve"].Binary) + } if snap.Permissions == nil { t.Fatal("expected non-nil permissions") } - mustDeepEqual(t, []string{"./photos/"}, snap.Permissions.Read) - mustDeepEqual(t, []string{"core/media"}, snap.Modules) + if !slices.Equal(snap.Permissions.Read, []string{"./photos/"}) { + t.Fatalf("permission reads = %v, want [./photos/]", snap.Permissions.Read) + } + if !slices.Equal(snap.Modules, []string{"core/media"}) { + t.Fatalf("modules = %v, want [core/media]", snap.Modules) + } } func TestGenerate_NoDaemons_Good(t *testing.T) { @@ -60,13 +85,21 @@ func TestGenerate_NoDaemons_Good(t *testing.T) { } data, err := GenerateAt(m, "abc123", "v0.1.0", fixedTime) - mustNoError(t, err) + if err != nil { + t.Fatalf("generate snapshot: %v", err) + } var snap Snapshot - mustNoError(t, json.Unmarshal(data, &snap)) + if err := json.Unmarshal(data, &snap); err != nil { + t.Fatalf("unmarshal snapshot: %v", err) + } - mustEqual(t, 1, snap.Schema) - mustEqual(t, "simple", snap.Code) + if snap.Schema != 1 { + t.Fatalf("schema = %d, want 1", snap.Schema) + } + if snap.Code != "simple" { + t.Fatalf("code = %q, want simple", snap.Code) + } if snap.Daemons != nil { t.Fatalf("expected nil daemons, got %v", snap.Daemons) } @@ -77,5 +110,10 @@ func TestGenerate_NoDaemons_Good(t *testing.T) { func TestGenerate_NilManifest_Bad(t *testing.T) { _, err := Generate(nil, "abc123", "v1.0.0") - mustErrorContains(t, err, "manifest is nil") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "manifest is nil") { + t.Fatalf("error = %q, want substring %q", err.Error(), "manifest is nil") + } } diff --git a/snapshot/test_helpers_test.go b/snapshot/test_helpers_test.go deleted file mode 100644 index 987f16a..0000000 --- a/snapshot/test_helpers_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package snapshot - -import ( - "reflect" - "strings" - "testing" -) - -func mustNoError(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func mustEqual[T comparable](t *testing.T, want, got T) { - t.Helper() - if want != got { - t.Fatalf("want %v, got %v", want, got) - } -} - -func mustDeepEqual(t *testing.T, want, got any) { - t.Helper() - if !reflect.DeepEqual(want, got) { - t.Fatalf("want %v, got %v", want, got) - } -} - -func mustLenMap[K comparable, V any](t *testing.T, m map[K]V, want int) { - t.Helper() - if len(m) != want { - t.Fatalf("want length %d, got %d", want, len(m)) - } -} - -func mustErrorContains(t *testing.T, err error, sub string) { - t.Helper() - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), sub) { - t.Fatalf("expected error %q to contain %q", err.Error(), sub) - } -}