From d9290b782114a92ed9c85644a503de020c6f934f Mon Sep 17 00:00:00 2001 From: caesarsage Date: Thu, 9 Jul 2026 14:58:11 +0100 Subject: [PATCH 1/4] refactor(errors): remove the legacy exit shim Signed-off-by: caesarsage --- pkg/errors/error.go | 40 ---------------------------------------- watcher/main.go | 12 +++++++++--- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/pkg/errors/error.go b/pkg/errors/error.go index c1e73dbc..57410643 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -19,48 +19,8 @@ package errors import ( stderrors "errors" "fmt" - "log" - "os" ) -// Deprecated: these numeric codes and the Check*/Fatal helpers below are the -// legacy exit mechanism. New code classifies failures with a Kind (see Wrap) and -// lets cmd.Handle map Kind -> exit code. Kept as a shim until every call site is -// migrated, then removed. -const ( - // ErrorCommandSpecific is reserved for command specific indications - ErrorCommandSpecific = 1 - // ErrorConnectionFailure is returned on connection failure to API endpoint - ErrorConnectionFailure = 11 - // ErrorAPIResponse is returned on unexpected API response, i.e. authorization failure - ErrorAPIResponse = 12 - // ErrorResourceDoesNotExist is returned when the requested resource does not exist - ErrorResourceDoesNotExist = 13 - // ErrorGeneric is returned for generic error - ErrorGeneric = 20 -) - -// Deprecated: return errors.Wrap(kind, err) from a RunE command instead. -func CheckError(err error) { - if err != nil { - Fatal(ErrorGeneric, err) - } -} - -// Deprecated: return a KindNotFound-wrapped error instead. -func CheckConfigNil(isNil bool, path string) { - if isNil { - Fatal(ErrorGeneric, "No contexts defined in "+path) - } -} - -// Deprecated: only main/cmd.Handle should exit the process. Fatal is a wrapper -// for log.Fatal() to exit with a custom code. -func Fatal(exitcode int, args ...interface{}) { - log.Println(args...) - os.Exit(exitcode) -} - // Kind classifies why an operation failed. The library returns kinds; the cmd // layer maps them to exit codes, so pkg/* never depends on exit codes and stays // safe to embed. diff --git a/watcher/main.go b/watcher/main.go index 54f06cd1..cfa92eaf 100644 --- a/watcher/main.go +++ b/watcher/main.go @@ -2,18 +2,24 @@ package main import ( "fmt" + "os" "github.com/microcks/microcks-cli/pkg/config" - "github.com/microcks/microcks-cli/pkg/errors" "github.com/microcks/microcks-cli/pkg/watcher" ) func main() { watchFile, err := config.DefaultLocalWatchPath() - errors.CheckError(err) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } wm, err := watcher.NewWatchManger(watchFile) - errors.CheckError(err) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } fmt.Println("[INFO] microcks-watcher started...") wm.Run() From 7043fbbfe68af18d1797ed33ac37441c2efef0f1 Mon Sep 17 00:00:00 2001 From: caesarsage Date: Thu, 9 Jul 2026 14:58:11 +0100 Subject: [PATCH 2/4] ci: run tests and guard against stray process exits Signed-off-by: caesarsage --- .github/workflows/build-verify.yml | 13 +++++++++++++ CONTRIBUTING.md | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/build-verify.yml b/.github/workflows/build-verify.yml index 3ad1a217..7c315413 100644 --- a/.github/workflows/build-verify.yml +++ b/.github/workflows/build-verify.yml @@ -41,6 +41,19 @@ jobs: make clean make build-binaries + - name: Run tests + run: go test ./... + + - name: Guard against stray process exits + run: | + # pkg/* and cmd/* (except cmd/exit.go) must return a classified error, + # never exit or panic. Only the main entrypoints exit the process. + # See documentation/error-handling.md. + if grep -rnE '(os\.Exit|log\.Fatal|panic\()' --include='*.go' cmd pkg | grep -vE '_test\.go|cmd/exit\.go'; then + echo "::error::os.Exit/log.Fatal/panic found outside cmd/exit.go — return errors.Wrap(kind, err) instead." + exit 1 + fi + - name: Set environment for branch run: | set -x diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 500bfc70..dff14b13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,13 @@ Microcks has adopted a Code of Conduct that we expect project participants to ad We use Github to host code, to track issues and feature requests, as well as accept pull requests. +## Error handling + +Code under `pkg/` and `cmd/` must return errors, never exit or panic on a runtime +error: wrap the failure with a Kind (`return errors.Wrap(errors.KindConnection, err)`) +and let it flow up. Only the `main` entrypoints and `cmd.Handle` exit the process. +See [documentation/error-handling.md](documentation/error-handling.md); CI enforces this. + ## Issues [Open an issue](https://github.com/microcks/microcks/issues/new) **only** if you want to report a bug or a feature. Don't open issues for questions or support, instead join our [Discord #support channel](https://microcks.io/discord-invite) or our [GitHub discussions](https://github.com/orgs/microcks/discussions) and ask there. From 884714b186835a335e2d10b294a6deeb4a22f1c7 Mon Sep 17 00:00:00 2001 From: caesarsage Date: Sun, 2 Aug 2026 11:02:30 +0100 Subject: [PATCH 3/4] chore(cli): added missing copyright headers Signed-off-by: caesarsage --- cmd/context.go | 16 ++++++++++++++++ cmd/context_test.go | 16 ++++++++++++++++ cmd/login.go | 16 ++++++++++++++++ cmd/logout.go | 16 ++++++++++++++++ cmd/logout_test.go | 16 ++++++++++++++++ cmd/start.go | 16 ++++++++++++++++ cmd/start_test.go | 16 ++++++++++++++++ cmd/stop.go | 16 ++++++++++++++++ main.go | 16 ++++++++++++++++ pkg/config/file_permission_unix.go | 16 ++++++++++++++++ pkg/config/file_permission_windows.go | 16 ++++++++++++++++ pkg/config/localconfig.go | 16 ++++++++++++++++ pkg/connectors/container_client.go | 16 ++++++++++++++++ pkg/connectors/microcks_client_test.go | 16 ++++++++++++++++ pkg/util/rand/rand.go | 16 ++++++++++++++++ pkg/util/util.go | 16 ++++++++++++++++ pkg/watcher/executor.go | 16 ++++++++++++++++ pkg/watcher/watchManager.go | 16 ++++++++++++++++ watcher/main.go | 16 ++++++++++++++++ 19 files changed, 304 insertions(+) diff --git a/cmd/context.go b/cmd/context.go index a83488df..8a7f35b2 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/cmd/context_test.go b/cmd/context_test.go index 396a3b91..2802f09c 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/cmd/login.go b/cmd/login.go index 0ed60429..34efcf74 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/cmd/logout.go b/cmd/logout.go index 2438f751..3d7bdd88 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/cmd/logout_test.go b/cmd/logout_test.go index 2801f445..d21a74ad 100644 --- a/cmd/logout_test.go +++ b/cmd/logout_test.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/cmd/start.go b/cmd/start.go index 9f544686..3dae5665 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/cmd/start_test.go b/cmd/start_test.go index 5e6e3d7a..ac381f62 100644 --- a/cmd/start_test.go +++ b/cmd/start_test.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/cmd/stop.go b/cmd/stop.go index 50a33afc..aa374875 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package cmd import ( diff --git a/main.go b/main.go index 99ad6357..e90256cf 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package main import ( diff --git a/pkg/config/file_permission_unix.go b/pkg/config/file_permission_unix.go index b22031d1..d474c397 100644 --- a/pkg/config/file_permission_unix.go +++ b/pkg/config/file_permission_unix.go @@ -1,5 +1,21 @@ //go:build !windows +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package config import ( diff --git a/pkg/config/file_permission_windows.go b/pkg/config/file_permission_windows.go index e92c374f..42b3c6ba 100644 --- a/pkg/config/file_permission_windows.go +++ b/pkg/config/file_permission_windows.go @@ -1,5 +1,21 @@ //go:build windows +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package config import ( diff --git a/pkg/config/localconfig.go b/pkg/config/localconfig.go index d02bfb92..b930704d 100644 --- a/pkg/config/localconfig.go +++ b/pkg/config/localconfig.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package config import ( diff --git a/pkg/connectors/container_client.go b/pkg/connectors/container_client.go index d2bfb61c..f6cdfe61 100644 --- a/pkg/connectors/container_client.go +++ b/pkg/connectors/container_client.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package connectors import ( diff --git a/pkg/connectors/microcks_client_test.go b/pkg/connectors/microcks_client_test.go index e3853bb1..09b2a246 100644 --- a/pkg/connectors/microcks_client_test.go +++ b/pkg/connectors/microcks_client_test.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package connectors import ( diff --git a/pkg/util/rand/rand.go b/pkg/util/rand/rand.go index 1e748bf9..ca921f1c 100644 --- a/pkg/util/rand/rand.go +++ b/pkg/util/rand/rand.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package rand import ( diff --git a/pkg/util/util.go b/pkg/util/util.go index 5c3b6636..8b1f0859 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package util import ( diff --git a/pkg/watcher/executor.go b/pkg/watcher/executor.go index 151e9d5f..87f2a98d 100644 --- a/pkg/watcher/executor.go +++ b/pkg/watcher/executor.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package watcher import ( diff --git a/pkg/watcher/watchManager.go b/pkg/watcher/watchManager.go index 6e0167c7..052a8ed6 100644 --- a/pkg/watcher/watchManager.go +++ b/pkg/watcher/watchManager.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package watcher import ( diff --git a/watcher/main.go b/watcher/main.go index cfa92eaf..59c6fcd0 100644 --- a/watcher/main.go +++ b/watcher/main.go @@ -1,3 +1,19 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package main import ( From 3de5886e2e4bfab2dd37b19dc670e08778165cb9 Mon Sep 17 00:00:00 2001 From: caesarsage Date: Sun, 2 Aug 2026 11:13:52 +0100 Subject: [PATCH 4/4] fix(cli): harden error handling and add policy guards Signed-off-by: caesarsage --- .github/workflows/build-verify.yml | 10 -- .github/workflows/policy-guards.yml | 62 +++++++++++++ cmd/context.go | 15 +-- cmd/login.go | 6 +- cmd/logout.go | 2 +- cmd/start.go | 4 +- cmd/stop.go | 8 +- cmd/testDryRun.go | 8 +- pkg/config/config.go | 4 +- pkg/connectors/container_client.go | 6 +- pkg/connectors/keycloak_client.go | 22 +++-- pkg/connectors/microcks_client.go | 86 +++++++++-------- pkg/connectors/microcks_client_test.go | 122 ++++++++++++++++++++++++- 13 files changed, 276 insertions(+), 79 deletions(-) create mode 100644 .github/workflows/policy-guards.yml diff --git a/.github/workflows/build-verify.yml b/.github/workflows/build-verify.yml index 7c315413..5039e7c3 100644 --- a/.github/workflows/build-verify.yml +++ b/.github/workflows/build-verify.yml @@ -44,16 +44,6 @@ jobs: - name: Run tests run: go test ./... - - name: Guard against stray process exits - run: | - # pkg/* and cmd/* (except cmd/exit.go) must return a classified error, - # never exit or panic. Only the main entrypoints exit the process. - # See documentation/error-handling.md. - if grep -rnE '(os\.Exit|log\.Fatal|panic\()' --include='*.go' cmd pkg | grep -vE '_test\.go|cmd/exit\.go'; then - echo "::error::os.Exit/log.Fatal/panic found outside cmd/exit.go — return errors.Wrap(kind, err) instead." - exit 1 - fi - - name: Set environment for branch run: | set -x diff --git a/.github/workflows/policy-guards.yml b/.github/workflows/policy-guards.yml new file mode 100644 index 00000000..72ea6961 --- /dev/null +++ b/.github/workflows/policy-guards.yml @@ -0,0 +1,62 @@ +name: policy-guards + +on: + push: + paths: + - '**/*.go' + - '.github/workflows/policy-guards.yml' + pull_request: + paths: + - '**/*.go' + - '.github/workflows/policy-guards.yml' + +permissions: + contents: read + +jobs: + policy-guards: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + + - name: Guard Go copyright headers + run: | + missing="" + while IFS= read -r file; do + if ! grep -q "Copyright The Microcks Authors" "$file"; then + missing+="${file}"$'\n' + fi + done < <(git ls-files '*.go') + + if [[ -n "$missing" ]]; then + echo "::error::Go files missing Microcks copyright header:" + printf "%s" "$missing" + exit 1 + fi + + - name: Guard against silently ignored errors + run: | + # Avoid reintroducing review issues where malformed responses or local + # failures are discarded instead of being returned as classified errors. + if grep -rnE '(^|[^[:alnum:]_])_ =|, _ :=|fmt\.Println\(err\)' --include='*.go' cmd pkg; then + echo "::error::Potential silently ignored error found. Handle it explicitly or return errors.Wrap(kind, err)." + exit 1 + fi + + - name: Guard against stray process exits + run: | + # Library and command packages must return classified errors, never + # exit or panic. Only the explicit process entrypoints may exit. + # See documentation/error-handling.md. + matches="$(git grep -nE '(os\.Exit|log\.Fatal|panic\()' -- '*.go' \ + ':!**/*_test.go' \ + ':!cmd/exit.go' \ + ':!main.go' \ + ':!watcher/main.go' || true)" + + if [[ -n "$matches" ]]; then + printf "%s\n" "$matches" + echo "::error::os.Exit/log.Fatal/panic found outside approved entrypoints — return errors.Wrap(kind, err) instead." + exit 1 + fi diff --git a/cmd/context.go b/cmd/context.go index 8a7f35b2..19796ca1 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -18,7 +18,6 @@ package cmd import ( "fmt" - "log" "os" "strings" "text/tabwriter" @@ -97,8 +96,8 @@ func deleteContext(context, configPath string) error { if !ok { return errors.Wrapf(errors.KindNotFound, "context %q does not exist", context) } - _ = localCfg.RemoveUser(context) - _ = localCfg.RemoveServer(serverName) + localCfg.RemoveUser(context) + localCfg.RemoveServer(serverName) if localCfg.IsEmpty() { if err := localCfg.DeleteLocalConfig(configPath); err != nil { @@ -128,24 +127,26 @@ func printMicrocksContexts(configPath string) error { return errors.Wrapf(errors.KindUsage, "no contexts defined in %s", configPath) } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - defer func() { _ = w.Flush() }() columnNames := []string{"CURRENT", "NAME", "SERVER"} if _, err = fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")); err != nil { - return err + return errors.Wrap(errors.KindEnvironment, fmt.Errorf("writing contexts output: %w", err)) } for _, contextRef := range localCfg.Contexts { context, err := localCfg.ResolveContext(contextRef.Name) if err != nil { - log.Printf("Context '%s' had error: %v", contextRef.Name, err) + return errors.Wrap(errors.KindUsage, fmt.Errorf("context %q is invalid: %w", contextRef.Name, err)) } prefix := " " if localCfg.CurrentContext == context.Name { prefix = "*" } if _, err = fmt.Fprintf(w, "%s\t%s\t%s\n", prefix, context.Name, context.Server.Server); err != nil { - return err + return errors.Wrap(errors.KindEnvironment, fmt.Errorf("writing contexts output: %w", err)) } } + if err := w.Flush(); err != nil { + return errors.Wrap(errors.KindEnvironment, fmt.Errorf("writing contexts output: %w", err)) + } return nil } diff --git a/cmd/login.go b/cmd/login.go index 34efcf74..ac8a96e4 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -169,7 +169,7 @@ microcks login http://localhost:8080 --sso --sso-launch-browser=false _, _, err = parser.ParseUnverified(authToken, &claims) if err != nil { - fmt.Println(err) + return errors.Wrap(errors.KindAPI, fmt.Errorf("parsing authentication token: %w", err)) } em := StringField(claims, "preferred_username") @@ -332,7 +332,9 @@ func oauth2login( fmt.Printf("Authentication successful\n") ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() - _ = srv.Shutdown(ctx) + if err := srv.Shutdown(ctx); err != nil { + return "", "", errors.Wrap(errors.KindEnvironment, fmt.Errorf("shutting down temporary HTTP server: %w", err)) + } return tokenString, refreshToken, nil } diff --git a/cmd/logout.go b/cmd/logout.go index 3d7bdd88..66fa4d85 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -74,7 +74,7 @@ func logoutContext(target, configPath string) error { err = config.ValidateLocalConfig(*localCfg) if err != nil { - return fmt.Errorf("Error in loging out: %s", err) + return errors.Wrap(errors.KindUsage, fmt.Errorf("logging out leaves local config invalid: %w", err)) } return config.WriteLocalConfig(*localCfg, configPath) diff --git a/cmd/start.go b/cmd/start.go index 3dae5665..6e216cf0 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -63,8 +63,8 @@ microcks start --name [name of you container/instance]`, localConfig = &config.LocalConfig{} } - instance, _ := localConfig.GetInstance(name) - if instance == nil { + instance, err := localConfig.GetInstance(name) + if err != nil { instance = &config.Instance{} } diff --git a/cmd/stop.go b/cmd/stop.go index aa374875..1a013253 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -75,10 +75,10 @@ func NewStopCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { if !ok { return errors.Wrapf(errors.KindNotFound, "context %q does not exist", ctx.Name) } - _ = localConfig.RemoveServer(ctx.Server.Server) - _ = localConfig.RemoveUser(ctx.User.Name) - _ = localConfig.RemoveAuth(ctx.Server.Server) - _ = localConfig.RemoveInstance(instance.Name) + localConfig.RemoveServer(ctx.Server.Server) + localConfig.RemoveUser(ctx.User.Name) + localConfig.RemoveAuth(ctx.Server.Server) + localConfig.RemoveInstance(instance.Name) localConfig.CurrentContext = "" log.Printf("Instance %s removed successfully", instance.Name) diff --git a/cmd/testDryRun.go b/cmd/testDryRun.go index 5f0144cb..fd6c4a55 100644 --- a/cmd/testDryRun.go +++ b/cmd/testDryRun.go @@ -100,15 +100,15 @@ func setupPodman() error { func validateDryRunOptions(opts dryRunOptions) error { if opts.artifact == "" { - return fmt.Errorf("--artifact is required with --dry-run") + return errors.Wrapf(errors.KindUsage, "--artifact is required with --dry-run") } if _, err := os.Stat(opts.artifact); err != nil { - return fmt.Errorf("cannot read --artifact file %q: %v", opts.artifact, err) + return errors.Wrap(errors.KindUsage, fmt.Errorf("cannot read --artifact file %q: %w", opts.artifact, err)) } // The uber-native flavor runs without Keycloak, which is what makes the // zero-config dry-run possible. Fail fast on other flavors. if !strings.Contains(opts.image, "-native") { - return fmt.Errorf("--dry-run requires the uber-native image variant (got %q). "+ + return errors.Wrapf(errors.KindUsage, "--dry-run requires the uber-native image variant (got %q). "+ "Use the default or pass --image with a *-native tag", opts.image) } return nil @@ -144,7 +144,7 @@ func rewriteLocalEndpoint(testEndpoint string) (string, int, bool) { func runDryRunTest(opts dryRunOptions) error { if err := validateDryRunOptions(opts); err != nil { - return errors.Wrap(errors.KindUsage, err) + return err } // Select the container runtime (docker default, podman wired via DOCKER_HOST). diff --git a/pkg/config/config.go b/pkg/config/config.go index cf610adc..dc165a5e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -50,8 +50,8 @@ func CreateTLSConfig() *tls.Config { } if len(CaCertPaths) > 0 { // Get the SystemCertPool, continue with an empty pool on error - rootCAs, _ := x509.SystemCertPool() - if rootCAs == nil { + rootCAs, err := x509.SystemCertPool() + if err != nil || rootCAs == nil { rootCAs = x509.NewCertPool() } diff --git a/pkg/connectors/container_client.go b/pkg/connectors/container_client.go index f6cdfe61..49c4ef2f 100644 --- a/pkg/connectors/container_client.go +++ b/pkg/connectors/container_client.go @@ -30,6 +30,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/go-connections/nat" + "github.com/microcks/microcks-cli/pkg/errors" "github.com/moby/term" ) @@ -141,7 +142,10 @@ func (cli *containerClient) CreateContainer(opts ContainerOpts) (string, error) ctx := context.Background() // Define exposed port and bindings - exposedPort, _ := nat.NewPort("tcp", "8080") + exposedPort, err := nat.NewPort("tcp", "8080") + if err != nil { + return "", errors.Wrap(errors.KindEnvironment, fmt.Errorf("creating exposed container port: %w", err)) + } portBindings := nat.PortMap{ exposedPort: []nat.PortBinding{ { diff --git a/pkg/connectors/keycloak_client.go b/pkg/connectors/keycloak_client.go index ed37aedb..185a8aac 100644 --- a/pkg/connectors/keycloak_client.go +++ b/pkg/connectors/keycloak_client.go @@ -147,10 +147,13 @@ func (c *keycloakClient) GetOIDCConfig() (*oauth2.Config, error) { return nil, errors.Wrap(errors.KindAPI, fmt.Errorf("parsing Keycloak OIDC config: %w", err)) } - authURL, _ := openIDResp["authorization_endpoint"].(string) - tokenURL, _ := openIDResp["token_endpoint"].(string) - if authURL == "" || tokenURL == "" { - return nil, errors.Wrapf(errors.KindAPI, "Keycloak OIDC config missing authorization_endpoint or token_endpoint") + authURL, ok := openIDResp["authorization_endpoint"].(string) + if !ok || authURL == "" { + return nil, errors.Wrapf(errors.KindAPI, "Keycloak OIDC config missing or invalid authorization_endpoint") + } + tokenURL, ok := openIDResp["token_endpoint"].(string) + if !ok || tokenURL == "" { + return nil, errors.Wrapf(errors.KindAPI, "Keycloak OIDC config missing or invalid token_endpoint") } return &oauth2.Config{ @@ -201,10 +204,13 @@ func (c *keycloakClient) ConnectAndGetTokenAndRefreshToken(username, password st return "", "", errors.Wrap(errors.KindAPI, fmt.Errorf("parsing Keycloak token response: %w", err)) } - authToken, _ := openIDResp["access_token"].(string) - refreshToken, _ := openIDResp["refresh_token"].(string) - if authToken == "" || refreshToken == "" { - return "", "", errors.Wrapf(errors.KindAPI, "Keycloak token response missing access_token or refresh_token") + authToken, ok := openIDResp["access_token"].(string) + if !ok || authToken == "" { + return "", "", errors.Wrapf(errors.KindAPI, "Keycloak token response missing or invalid access_token") + } + refreshToken, ok := openIDResp["refresh_token"].(string) + if !ok || refreshToken == "" { + return "", "", errors.Wrapf(errors.KindAPI, "Keycloak token response missing or invalid refresh_token") } return authToken, refreshToken, nil diff --git a/pkg/connectors/microcks_client.go b/pkg/connectors/microcks_client.go index b13d574e..c6bed108 100644 --- a/pkg/connectors/microcks_client.go +++ b/pkg/connectors/microcks_client.go @@ -220,7 +220,7 @@ func (c *microcksClient) GetKeycloakURL() (string, error) { req, err := http.NewRequest("GET", u.String(), nil) if err != nil { - return "", err + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("creating Keycloak config request: %w", err)) } req.Header.Set("Accept", "application/json") @@ -260,10 +260,13 @@ func (c *microcksClient) GetKeycloakURL() (string, error) { return "null", nil } - authServerURL, _ := configResp["auth-server-url"].(string) - realmName, _ := configResp["realm"].(string) - if authServerURL == "" || realmName == "" { - return "", errors.Wrapf(errors.KindAPI, "Keycloak config response missing auth-server-url or realm") + authServerURL, ok := configResp["auth-server-url"].(string) + if !ok || authServerURL == "" { + return "", errors.Wrapf(errors.KindAPI, "Keycloak config response missing or invalid auth-server-url field") + } + realmName, ok := configResp["realm"].(string) + if !ok || realmName == "" { + return "", errors.Wrapf(errors.KindAPI, "Keycloak config response missing or invalid realm field") } return authServerURL + "/realms/" + realmName + "/", nil } @@ -359,24 +362,33 @@ func (c *microcksClient) CreateTestResult(serviceID string, testEndpoint string, SecretName: secretName, } - if len(filteredOperations) > 0 && ensureValidOperationsList(filteredOperations) { + if len(filteredOperations) > 0 { + if err := ensureValidOperationsList(filteredOperations); err != nil { + return "", err + } testReq.FilteredOperations = json.RawMessage(filteredOperations) } - if len(operationsHeaders) > 0 && ensureValidOperationsHeaders(operationsHeaders) { + if len(operationsHeaders) > 0 { + if err := ensureValidOperationsHeaders(operationsHeaders); err != nil { + return "", err + } testReq.OperationsHeaders = json.RawMessage(operationsHeaders) } - if len(oAuth2Context) > 0 && ensureValidOAuth2Context(oAuth2Context) { + if len(oAuth2Context) > 0 { + if err := ensureValidOAuth2Context(oAuth2Context); err != nil { + return "", err + } testReq.OAuth2Context = json.RawMessage(oAuth2Context) } input, err := json.Marshal(testReq) if err != nil { - return "", fmt.Errorf("failed to marshal test request: %w", err) + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("failed to marshal test request: %w", err)) } req, err := http.NewRequest("POST", u.String(), bytes.NewReader(input)) if err != nil { - return "", err + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("creating test request: %w", err)) } req.Header.Set("Content-Type", "application/json; charset=utf-8") @@ -408,12 +420,12 @@ func (c *microcksClient) CreateTestResult(serviceID string, testEndpoint string, var createTestResp map[string]interface{} if err := json.Unmarshal(body, &createTestResp); err != nil { - return "", fmt.Errorf("failed to parse test creation response: %w", err) + return "", errors.Wrap(errors.KindAPI, fmt.Errorf("failed to parse test creation response: %w", err)) } testID, ok := createTestResp["id"].(string) if !ok || testID == "" { - return "", fmt.Errorf("microcks response missing 'id' field") + return "", errors.Wrapf(errors.KindAPI, "microcks response missing 'id' field") } return testID, nil } @@ -425,7 +437,7 @@ func (c *microcksClient) GetTestResult(testResultID string) (*TestResultSummary, req, err := http.NewRequest("GET", u.String(), nil) if err != nil { - return nil, err + return nil, errors.Wrap(errors.KindGeneric, fmt.Errorf("creating test result request: %w", err)) } req.Header.Set("Accept", "application/json") @@ -450,7 +462,7 @@ func (c *microcksClient) GetTestResult(testResultID string) (*TestResultSummary, result := TestResultSummary{} if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to parse test result response: %w", err) + return nil, errors.Wrap(errors.KindAPI, fmt.Errorf("failed to parse test result response: %w", err)) } return &result, nil @@ -500,7 +512,7 @@ func (c *microcksClient) UploadArtifact(specificationFilePath string, mainArtifa req, err := http.NewRequest("POST", u.String(), pr) if err != nil { - return "", err + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("creating artifact upload request: %w", err)) } req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+c.AuthToken) @@ -516,7 +528,7 @@ func (c *microcksClient) UploadArtifact(specificationFilePath string, mainArtifa // Check for errors from the multipart writer goroutine. if pipeErr := <-errCh; pipeErr != nil { - return "", fmt.Errorf("failed to write multipart form: %w", pipeErr) + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("failed to write multipart form: %w", pipeErr)) } // Dump response if verbose required. @@ -524,7 +536,7 @@ func (c *microcksClient) UploadArtifact(specificationFilePath string, mainArtifa respBody, err := io.ReadAll(resp.Body) if err != nil { - return "", fmt.Errorf("failed to read upload response: %w", err) + return "", errors.Wrap(errors.KindConnection, fmt.Errorf("failed to read upload response: %w", err)) } // Raise exception if not created. @@ -542,15 +554,21 @@ func (c *microcksClient) DownloadArtifact(artifactURL string, mainArtifact bool, writer := multipart.NewWriter(body) // Add all the form fields - writer.WriteField("url", artifactURL) - writer.WriteField("mainArtifact", strconv.FormatBool(mainArtifact)) + if err := writer.WriteField("url", artifactURL); err != nil { + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("writing artifact URL field: %w", err)) + } + if err := writer.WriteField("mainArtifact", strconv.FormatBool(mainArtifact)); err != nil { + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("writing mainArtifact field: %w", err)) + } if secret != "" { - writer.WriteField("secret", secret) + if err := writer.WriteField("secret", secret); err != nil { + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("writing secret field: %w", err)) + } } err := writer.Close() if err != nil { - return "", err + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("closing artifact download form: %w", err)) } // Ensure we have a correct URL. @@ -559,7 +577,7 @@ func (c *microcksClient) DownloadArtifact(artifactURL string, mainArtifact bool, req, err := http.NewRequest("POST", u.String(), body) if err != nil { - return "", err + return "", errors.Wrap(errors.KindGeneric, fmt.Errorf("creating artifact download request: %w", err)) } req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+c.AuthToken) @@ -589,38 +607,34 @@ func (c *microcksClient) DownloadArtifact(artifactURL string, mainArtifact bool, return string(respBody), nil } -func ensureValidOperationsList(filteredOperations string) bool { +func ensureValidOperationsList(filteredOperations string) error { // Unmarshal using a generic interface var list = []string{} err := json.Unmarshal([]byte(filteredOperations), &list) if err != nil { - fmt.Println("Error parsing JSON in filteredOperations: ", err) - return false + return errors.Wrap(errors.KindUsage, fmt.Errorf("parsing filteredOperations JSON: %w", err)) } - return true + return nil } -func ensureValidOperationsHeaders(operationsHeaders string) bool { +func ensureValidOperationsHeaders(operationsHeaders string) error { // Unmarshal using a generic interface var headers = map[string][]HeaderDTO{} err := json.Unmarshal([]byte(operationsHeaders), &headers) if err != nil { - fmt.Println("Error parsing JSON in operationsHeaders: ", err) - return false + return errors.Wrap(errors.KindUsage, fmt.Errorf("parsing operationsHeaders JSON: %w", err)) } - return true + return nil } -func ensureValidOAuth2Context(oAuth2Context string) bool { +func ensureValidOAuth2Context(oAuth2Context string) error { var oContext = OAuth2ClientContext{} err := json.Unmarshal([]byte(oAuth2Context), &oContext) if err != nil { - fmt.Println("Error parsing JSON in oAuth2Context: ", err) - return false + return errors.Wrap(errors.KindUsage, fmt.Errorf("parsing oAuth2Context JSON: %w", err)) } if !grantTypeChoices[oContext.GrantType] { - fmt.Println("grantType in oAuth2Context is not supported. OAuth2 is turned off.") - return false + return errors.Wrapf(errors.KindUsage, "grantType in oAuth2Context is not supported") } - return true + return nil } diff --git a/pkg/connectors/microcks_client_test.go b/pkg/connectors/microcks_client_test.go index 09b2a246..67f435e7 100644 --- a/pkg/connectors/microcks_client_test.go +++ b/pkg/connectors/microcks_client_test.go @@ -24,6 +24,8 @@ import ( "path/filepath" "strings" "testing" + + microckserrors "github.com/microcks/microcks-cli/pkg/errors" ) func TestUploadArtifactStreamsWithoutBuffering(t *testing.T) { @@ -70,7 +72,9 @@ func TestUploadArtifactStreamsWithoutBuffering(t *testing.T) { } w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte(expectedResponse)) + if _, err := w.Write([]byte(expectedResponse)); err != nil { + t.Fatalf("failed to write response: %v", err) + } })) defer server.Close() @@ -107,7 +111,9 @@ func TestDownloadArtifactReturnsResponseBody(t *testing.T) { t.Fatalf("unexpected mainArtifact value: %s", got) } w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte(expectedBody)) + if _, err := w.Write([]byte(expectedBody)); err != nil { + t.Fatalf("failed to write response: %v", err) + } })) defer server.Close() @@ -124,3 +130,115 @@ func TestDownloadArtifactReturnsResponseBody(t *testing.T) { t.Fatalf("expected response body %q, got %q", expectedBody, msg) } } + +func TestGetKeycloakURLRejectsMalformedConfig(t *testing.T) { + tests := []struct { + name string + body string + want string + }{ + { + name: "missing enabled", + body: `{"auth-server-url":"http://keycloak","realm":"microcks"}`, + want: "enabled", + }, + { + name: "invalid auth server url", + body: `{"enabled":true,"auth-server-url":42,"realm":"microcks"}`, + want: "auth-server-url", + }, + { + name: "invalid realm", + body: `{"enabled":true,"auth-server-url":"http://keycloak","realm":42}`, + want: "realm", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/keycloak/config" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write([]byte(tt.body)); err != nil { + t.Fatalf("failed to write response: %v", err) + } + })) + defer server.Close() + + client, err := NewMicrocksClient(server.URL) + if err != nil { + t.Fatalf("NewMicrocksClient returned error: %v", err) + } + + _, err = client.GetKeycloakURL() + if err == nil { + t.Fatal("GetKeycloakURL returned nil error") + } + if got := microckserrors.KindOf(err); got != microckserrors.KindAPI { + t.Fatalf("KindOf = %v, want %v", got, microckserrors.KindAPI) + } + if !strings.Contains(err.Error(), tt.want) { + t.Fatalf("error %q does not mention %q", err.Error(), tt.want) + } + }) + } +} + +func TestCreateTestResultClassifiesMalformedResponses(t *testing.T) { + tests := []struct { + name string + body string + want string + }{ + {name: "invalid json", body: `not-json`, want: "parse test creation response"}, + {name: "missing id", body: `{}`, want: "missing 'id'"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/tests" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusCreated) + if _, err := w.Write([]byte(tt.body)); err != nil { + t.Fatalf("failed to write response: %v", err) + } + })) + defer server.Close() + + client, err := NewMicrocksClient(server.URL) + if err != nil { + t.Fatalf("NewMicrocksClient returned error: %v", err) + } + + _, err = client.CreateTestResult("service:1.0", "http://example.test", "OPEN_API_SCHEMA", "", 1000, "", "", "") + if err == nil { + t.Fatal("CreateTestResult returned nil error") + } + if got := microckserrors.KindOf(err); got != microckserrors.KindAPI { + t.Fatalf("KindOf = %v, want %v", got, microckserrors.KindAPI) + } + if !strings.Contains(err.Error(), tt.want) { + t.Fatalf("error %q does not mention %q", err.Error(), tt.want) + } + }) + } +} + +func TestCreateTestResultRejectsInvalidFilteredOperations(t *testing.T) { + client, err := NewMicrocksClient("http://localhost:8585") + if err != nil { + t.Fatalf("NewMicrocksClient returned error: %v", err) + } + + _, err = client.CreateTestResult("service:1.0", "http://example.test", "OPEN_API_SCHEMA", "", 1000, "{", "", "") + if err == nil { + t.Fatal("CreateTestResult returned nil error") + } + if got := microckserrors.KindOf(err); got != microckserrors.KindUsage { + t.Fatalf("KindOf = %v, want %v", got, microckserrors.KindUsage) + } +}