Repository: ory/kratos
Pull request #1469 (2021-06-29 08:21:30)

feat: add helper for starting kratos e2e
## Related issue

<!--
Please link the GitHub issue this pull request resolves in the format of `#1234`. If you discussed this change
with a maintainer, please mention her/him using the `@` syntax (e.g. `@aeneasr`).

If this change neither resolves an existing issue nor has sign-off from one of the maintainers, there is a
chance substantial changes will be requested or that the changes will be rejected.

You can discuss changes with maintainers either in the Github Discusssions in this repository or
join the [Ory Chat](https://www.ory.sh/chat).
-->

## Proposed changes

<!--
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.

If your change includes breaking changes please add a codeblock documenting the breaking change:

```
BREAKING CHANGES: This patch changes the behavior of configuration item `foo` to do bar. To keep the existing
behavior please do baz.
```
-->

## Checklist

<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of
them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.
-->

- [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md).
- [ ] I am following the
      [contributing code guidelines](../blob/master/CONTRIBUTING.md#contributing-code).
- [ ] I have read the [security policy](../security/policy).
- [ ] I confirm that this pull request does not address a security
      vulnerability. If this pull request addresses a security. vulnerability, I
      confirm that I got green light (please contact
      [security@ory.sh](mailto:security@ory.sh)) from the maintainers to push
      the changes.
- [ ] I have added tests that prove my fix is effective or that my feature
      works.
- [ ] I have added or changed [the documentation](docs/docs).

## Further comments

<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->



Patch:
diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go
index 312d7305e..e9d249821 100644
--- a/cmd/clidoc/main.go
+++ b/cmd/clidoc/main.go
@@ -10,7 +10,7 @@ import (
 )
 
 func main() {
-	if err := clidoc.Generate(cmd.RootCmd, os.Args[1:]); err != nil {
+	if err := clidoc.Generate(cmd.NewRootCmd(), os.Args[1:]); err != nil {
 		_, _ = fmt.Fprintf(os.Stderr, "%+v", err)
 		os.Exit(1)
 	}
diff --git a/cmd/courier/root.go b/cmd/courier/root.go
index bdca30a28..815dfea85 100644
--- a/cmd/courier/root.go
+++ b/cmd/courier/root.go
@@ -6,18 +6,18 @@ import (
 	"github.com/ory/x/configx"
 )
 
-// courierCmd represents the courier command
-var courierCmd = &cobra.Command{
-	Use:   "courier",
-	Short: "Commands related to the Ory Kratos message courier",
-}
-
-func init() {
-	configx.RegisterFlags(courierCmd.PersistentFlags())
+// NewCourierCmd creates a new courier command
+func NewCourierCmd() *cobra.Command {
+	c := &cobra.Command{
+		Use:   "courier",
+		Short: "Commands related to the Ory Kratos message courier",
+	}
+	configx.RegisterFlags(c.PersistentFlags())
+	return c
 }
 
 func RegisterCommandRecursive(parent *cobra.Command) {
-	parent.AddCommand(courierCmd)
-
-	courierCmd.AddCommand(watchCmd)
+	c := NewCourierCmd()
+	parent.AddCommand(c)
+	c.AddCommand(NewWatchCmd())
 }
diff --git a/cmd/courier/watch.go b/cmd/courier/watch.go
index 964de0b6a..0405c41af 100644
--- a/cmd/courier/watch.go
+++ b/cmd/courier/watch.go
@@ -15,17 +15,17 @@ import (
 	"github.com/ory/x/reqlog"
 )
 
-var watchCmd = &cobra.Command{
-	Use:   "watch",
-	Short: "Starts the Ory Kratos message courier",
-	Run: func(cmd *cobra.Command, args []string) {
-		r := driver.New(cmd.Context(), configx.WithFlags(cmd.Flags()))
-		StartCourier(cmd.Context(), r)
-	},
-}
-
-func init() {
-	watchCmd.PersistentFlags().Int("expose-metrics-port", 0, "The port to expose the metrics endpoint on (not exposed by default)")
+func NewWatchCmd() *cobra.Command {
+	var c = &cobra.Command{
+		Use:   "watch",
+		Short: "Starts the Ory Kratos message courier",
+		Run: func(cmd *cobra.Command, args []string) {
+			r := driver.New(cmd.Context(), configx.WithFlags(cmd.Flags()))
+			StartCourier(cmd.Context(), r)
+		},
+	}
+	c.PersistentFlags().Int("expose-metrics-port", 0, "The port to expose the metrics endpoint on (not exposed by default)")
+	return c
 }
 
 func StartCourier(ctx cx.Context, r driver.Registry) {
@@ -83,7 +83,6 @@ func ServeMetrics(ctx cx.Context, r driver.Registry) {
 }
 
 func Watch(ctx cx.Context, r driver.Registry) {
-
 	ctx, cancel := cx.WithCancel(ctx)
 
 	r.Logger().Println("Courier worker started.")
@@ -97,5 +96,4 @@ func Watch(ctx cx.Context, r driver.Registry) {
 	}
 
 	r.Logger().Println("Courier worker was shutdown gracefully.")
-
 }
diff --git a/cmd/hashers/root.go b/cmd/hashers/root.go
index f42c2a9d8..0c50e4356 100644
--- a/cmd/hashers/root.go
+++ b/cmd/hashers/root.go
@@ -6,13 +6,16 @@ import (
 	"github.com/ory/kratos/cmd/hashers/argon2"
 )
 
-var rootCmd = &cobra.Command{
-	Use:   "hashers",
-	Short: "This command contains helpers around hashing",
+func NewRootCmd() *cobra.Command {
+	c := &cobra.Command{
+		Use:   "hashers",
+		Short: "This command contains helpers around hashing",
+	}
+	return c
 }
 
 func RegisterCommandRecursive(parent *cobra.Command) {
-	parent.AddCommand(rootCmd)
+	parent.AddCommand(NewRootCmd())
 
-	argon2.RegisterCommandRecursive(rootCmd)
+	argon2.RegisterCommandRecursive(NewRootCmd())
 }
diff --git a/cmd/identities/delete.go b/cmd/identities/delete.go
index eed9447ac..9b01b0593 100644
--- a/cmd/identities/delete.go
+++ b/cmd/identities/delete.go
@@ -10,45 +10,47 @@ import (
 	"github.com/ory/x/cmdx"
 )
 
-var DeleteCmd = &cobra.Command{
-	Use:   "delete <id-0 [id-1 ...]>",
-	Short: "Delete identities by ID",
-	Long: fmt.Sprintf(`This command deletes one or more identities by ID. To delete an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
+func NewDeleteCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:   "delete <id-0 [id-1 ...]>",
+		Short: "Delete identities by ID",
+		Long: fmt.Sprintf(`This command deletes one or more identities by ID. To delete an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
 
 %s
 `, clihelpers.WarningJQIsComplicated),
-	Example: `To delete the identity with the recovery email address "foo@bar.com", run:
+		Example: `To delete the identity with the recovery email address "foo@bar.com", run:
 
 	$ kratos identities delete $(kratos identities list --format json | jq -r 'map(select(.recovery_addresses[].value == "foo@bar.com")) | .[].id')`,
-	Args: cobra.MinimumNArgs(1),
-	RunE: func(cmd *cobra.Command, args []string) error {
-		c := cliclient.NewClient(cmd)
-
-		var (
-			deleted = make([]string, 0, len(args))
-			errs    []error
-		)
-
-		for _, a := range args {
-			_, err := c.AdminApi.DeleteIdentity(cmd.Context(), a).Execute()
-			if err != nil {
-				errs = append(errs, err)
-				continue
+		Args: cobra.MinimumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c := cliclient.NewClient(cmd)
+
+			var (
+				deleted = make([]string, 0, len(args))
+				errs    []error
+			)
+
+			for _, a := range args {
+				_, err := c.AdminApi.DeleteIdentity(cmd.Context(), a).Execute()
+				if err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				deleted = append(deleted, a)
 			}
-			deleted = append(deleted, a)
-		}
-
-		for _, d := range deleted {
-			_, _ = fmt.Fprintln(cmd.OutOrStdout(), d)
-		}
-
-		for _, err := range errs {
-			_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%+v\n", err)
-		}
-
-		if len(errs) != 0 {
-			return cmdx.FailSilently(cmd)
-		}
-		return nil
-	},
+
+			for _, d := range deleted {
+				_, _ = fmt.Fprintln(cmd.OutOrStdout(), d)
+			}
+
+			for _, err := range errs {
+				_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%+v\n", err)
+			}
+
+			if len(errs) != 0 {
+				return cmdx.FailSilently(cmd)
+			}
+			return nil
+		},
+	}
 }
diff --git a/cmd/identities/get.go b/cmd/identities/get.go
index c854464b3..fe14fbc76 100644
--- a/cmd/identities/get.go
+++ b/cmd/identities/get.go
@@ -14,42 +14,44 @@ import (
 	"github.com/ory/kratos/cmd/cliclient"
 )
 
-var GetCmd = &cobra.Command{
-	Use:   "get <id-0 [id-1 ...]>",
-	Short: "Get one or more identities by ID",
-	Long: fmt.Sprintf(`This command gets all the details about an identity. To get an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
+func NewGetCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:   "get <id-0 [id-1 ...]>",
+		Short: "Get one or more identities by ID",
+		Long: fmt.Sprintf(`This command gets all the details about an identity. To get an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
 
 %s
 `, clihelpers.WarningJQIsComplicated),
-	Example: `To get the identities with the recovery email address at the domain "ory.sh", run:
+		Example: `To get the identities with the recovery email address at the domain "ory.sh", run:
 
 	$ kratos identities get $(kratos identities list --format json | jq -r 'map(select(.recovery_addresses[].value | endswith("@ory.sh"))) | .[].id')`,
-	Args: cobra.MinimumNArgs(1),
-	RunE: func(cmd *cobra.Command, args []string) error {
-		c := cliclient.NewClient(cmd)
-
-		identities := make([]kratos.Identity, 0, len(args))
-		failed := make(map[string]error)
-		for _, id := range args {
-			identity, _, err := c.AdminApi.GetIdentity(cmd.Context(), id).Execute()
-			if x.SDKError(err) != nil {
-				failed[id] = err
-				continue
+		Args: cobra.MinimumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c := cliclient.NewClient(cmd)
+
+			identities := make([]kratos.Identity, 0, len(args))
+			failed := make(map[string]error)
+			for _, id := range args {
+				identity, _, err := c.AdminApi.GetIdentity(cmd.Context(), id).Execute()
+				if x.SDKError(err) != nil {
+					failed[id] = err
+					continue
+				}
+
+				identities = append(identities, *identity)
 			}
 
-			identities = append(identities, *identity)
-		}
-
-		if len(identities) == 1 {
-			cmdx.PrintRow(cmd, (*outputIdentity)(&identities[0]))
-		} else if len(identities) > 1 {
-			cmdx.PrintTable(cmd, &outputIdentityCollection{identities})
-		}
-		cmdx.PrintErrors(cmd, failed)
-
-		if len(failed) != 0 {
-			return cmdx.FailSilently(cmd)
-		}
-		return nil
-	},
+			if len(identities) == 1 {
+				cmdx.PrintRow(cmd, (*outputIdentity)(&identities[0]))
+			} else if len(identities) > 1 {
+				cmdx.PrintTable(cmd, &outputIdentityCollection{identities})
+			}
+			cmdx.PrintErrors(cmd, failed)
+
+			if len(failed) != 0 {
+				return cmdx.FailSilently(cmd)
+			}
+			return nil
+		},
+	}
 }
diff --git a/cmd/identities/helpers.go b/cmd/identities/helpers.go
index a7a11cacf..aabe9770b 100644
--- a/cmd/identities/helpers.go
+++ b/cmd/identities/helpers.go
@@ -1,28 +1,13 @@
 package identities
 
 import (
-	"bytes"
-	"context"
 	"fmt"
-	"io"
 	"io/ioutil"
-	"testing"
-
-	"github.com/ory/x/cmdx"
-
-	"github.com/pkg/errors"
-	"github.com/tidwall/gjson"
-
-	"github.com/ory/kratos/identity"
 
 	"github.com/spf13/cobra"
-	"github.com/stretchr/testify/require"
+	"github.com/tidwall/gjson"
 
-	"github.com/ory/kratos/cmd/cliclient"
-	"github.com/ory/kratos/driver"
-	"github.com/ory/kratos/driver/config"
-	"github.com/ory/kratos/internal"
-	"github.com/ory/kratos/internal/testhelpers"
+	"github.com/ory/x/cmdx"
 )
 
 func parseIdentities(raw []byte) (rawIdentities []string) {
@@ -62,53 +47,3 @@ func readIdentities(cmd *cobra.Command, args []string) (map[string]string, error
 	}
 	return rawIdentities, nil
 }
-
-func setup(t *testing.T, cmd *cobra.Command) driver.Registry {
-	conf, reg := internal.NewFastRegistryWithMocks(t)
-	_, admin := testhelpers.NewKratosServerWithCSRF(t, reg)
-	conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stubs/identity.schema.json")
-	// setup command
-	cliclient.RegisterClientFlags(cmd.Flags())
-	cmdx.RegisterFormatFlags(cmd.Flags())
-	require.NoError(t, cmd.Flags().Set(cliclient.FlagEndpoint, admin.URL))
-	require.NoError(t, cmd.Flags().Set(cmdx.FlagFormat, string(cmdx.FormatJSON)))
-	return reg
-}
-
-func exec(cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) {
-	stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{}
-	cmd.SetErr(stdErr)
-	cmd.SetOut(stdOut)
-	cmd.SetIn(stdIn)
-	defer cmd.SetIn(nil)
-	if args == nil {
-		args = []string{}
-	}
-	cmd.SetArgs(args)
-	err := cmd.Execute()
-	return stdOut.String(), stdErr.String(), err
-}
-
-func execNoErr(t *testing.T, cmd *cobra.Command, args ...string) string {
-	stdOut, stdErr, err := exec(cmd, nil, args...)
-	require.NoError(t, err)
-	require.Len(t, stdErr, 0, stdOut)
-	return stdOut
-}
-
-func execErr(t *testing.T, cmd *cobra.Command, args ...string) string {
-	stdOut, stdErr, err := exec(cmd, nil, args...)
-	require.True(t, errors.Is(err, cmdx.ErrNoPrintButFail))
-	require.Len(t, stdOut, 0, stdErr)
-	return stdErr
-}
-
-func makeIdentities(t *testing.T, reg driver.Registry, n int) (is []*identity.Identity, ids []string) {
-	for j := 0; j < n; j++ {
-		i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
-		require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
-		is = append(is, i)
-		ids = append(ids, i.ID.String())
-	}
-	return
-}
diff --git a/cmd/identities/import.go b/cmd/identities/import.go
index 6ff21cada..e11aa8895 100644
--- a/cmd/identities/import.go
+++ b/cmd/identities/import.go
@@ -15,11 +15,12 @@ import (
 	"github.com/ory/kratos/cmd/cliclient"
 )
 
-// ImportCmd represents the import command
-var ImportCmd = &cobra.Command{
-	Use:   "import <file.json [file-2.json [file-3.json] ...]>",
-	Short: "Import identities from files or STD_IN",
-	Example: `$ cat > ./file.json <<EOF
+// NewImportCmd represents the import command
+func NewImportCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:   "import <file.json [file-2.json [file-3.json] ...]>",
+		Short: "Import identities from files or STD_IN",
+		Example: `$ cat > ./file.json <<EOF
 {
     "schema_id": "default",
     "traits": {
@@ -31,55 +32,56 @@ EOF
 $ kratos identities import file.json
 # Alternatively:
 $ cat file.json | kratos identities import`,
-	Long: `Import identities from files or STD_IN.
+		Long: `Import identities from files or STD_IN.
 
 Files can contain only a single or an array of identities. The validity of files can be tested beforehand using "... identities validate".
 
 WARNING: Importing credentials is not yet supported.`,
-	RunE: func(cmd *cobra.Command, args []string) error {
-		c := cliclient.NewClient(cmd)
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c := cliclient.NewClient(cmd)
 
-		imported := make([]kratos.Identity, 0, len(args))
-		failed := make(map[string]error)
+			imported := make([]kratos.Identity, 0, len(args))
+			failed := make(map[string]error)
 
-		is, err := readIdentities(cmd, args)
-		if err != nil {
-			return err
-		}
-
-		for src, i := range is {
-			err = validateIdentity(cmd, src, i, func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error) {
-				return c.PublicApi.GetSchema(ctx, id).Execute()
-			})
+			is, err := readIdentities(cmd, args)
 			if err != nil {
 				return err
 			}
 
-			var params kratos.CreateIdentity
-			err = json.Unmarshal([]byte(i), &params)
-			if err != nil {
-				_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "STD_IN: Could not parse identity")
-				return cmdx.FailSilently(cmd)
+			for src, i := range is {
+				err = ValidateIdentity(cmd, src, i, func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error) {
+					return c.PublicApi.GetSchema(ctx, id).Execute()
+				})
+				if err != nil {
+					return err
+				}
+
+				var params kratos.CreateIdentity
+				err = json.Unmarshal([]byte(i), &params)
+				if err != nil {
+					_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "STD_IN: Could not parse identity")
+					return cmdx.FailSilently(cmd)
+				}
+
+				ident, _, err := c.AdminApi.CreateIdentity(cmd.Context()).CreateIdentity(params).Execute()
+				if err != nil {
+					failed[src] = err
+				} else {
+					imported = append(imported, *ident)
+				}
 			}
-
-			ident, _, err := c.AdminApi.CreateIdentity(cmd.Context()).CreateIdentity(params).Execute()
-			if err != nil {
-				failed[src] = err
+			if len(imported) == 1 {
+				cmdx.PrintRow(cmd, (*outputIdentity)(&imported[0]))
 			} else {
-				imported = append(imported, *ident)
+				cmdx.PrintTable(cmd, &outputIdentityCollection{identities: imported})
 			}
-		}
-		if len(imported) == 1 {
-			cmdx.PrintRow(cmd, (*outputIdentity)(&imported[0]))
-		} else {
-			cmdx.PrintTable(cmd, &outputIdentityCollection{identities: imported})
-		}
-		cmdx.PrintErrors(cmd, failed)
-
-		if len(failed) != 0 {
-			return cmdx.FailSilently(cmd)
-		}
-
-		return nil
-	},
+			cmdx.PrintErrors(cmd, failed)
+
+			if len(failed) != 0 {
+				return cmdx.FailSilently(cmd)
+			}
+
+			return nil
+		},
+	}
 }
diff --git a/cmd/identities/list.go b/cmd/identities/list.go
index 60c254683..a571fc382 100644
--- a/cmd/identities/list.go
+++ b/cmd/identities/list.go
@@ -11,48 +11,50 @@ import (
 	"github.com/ory/kratos/cmd/cliclient"
 )
 
-var ListCmd = &cobra.Command{
-	Use:   "list [<page> <per-page>]",
-	Short: "List identities",
-	Long:  "List identities (paginated)",
-	Args: func(cmd *cobra.Command, args []string) error {
-		// zero or exactly two args
-		if len(args) != 0 && len(args) != 2 {
-			return fmt.Errorf("expected zero or two args, got %d: %+v", len(args), args)
-		}
-		return nil
-	},
-	Aliases: []string{"ls"},
-	RunE: func(cmd *cobra.Command, args []string) error {
-		c := cliclient.NewClient(cmd)
-		req := c.AdminApi.ListIdentities(cmd.Context())
-
-		if len(args) == 2 {
-			page, err := strconv.ParseInt(args[0], 0, 64)
-			if err != nil {
-				_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse page argument\"%s\": %s", args[0], err)
-				return cmdx.FailSilently(cmd)
+func NewListCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:   "list [<page> <per-page>]",
+		Short: "List identities",
+		Long:  "List identities (paginated)",
+		Args: func(cmd *cobra.Command, args []string) error {
+			// zero or exactly two args
+			if len(args) != 0 && len(args) != 2 {
+				return fmt.Errorf("expected zero or two args, got %d: %+v", len(args), args)
+			}
+			return nil
+		},
+		Aliases: []string{"ls"},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c := cliclient.NewClient(cmd)
+			req := c.AdminApi.ListIdentities(cmd.Context())
+
+			if len(args) == 2 {
+				page, err := strconv.ParseInt(args[0], 0, 64)
+				if err != nil {
+					_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse page argument\"%s\": %s", args[0], err)
+					return cmdx.FailSilently(cmd)
+				}
+				req = req.Page(page)
+
+				perPage, err := strconv.ParseInt(args[1], 0, 64)
+				if err != nil {
+					_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse per-page argument\"%s\": %s", args[1], err)
+					return cmdx.FailSilently(cmd)
+				}
+				req = req.PerPage(perPage)
 			}
-			req = req.Page(page)
 
-			perPage, err := strconv.ParseInt(args[1], 0, 64)
+			identities, _, err := req.Execute()
 			if err != nil {
-				_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse per-page argument\"%s\": %s", args[1], err)
+				_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the identities: %+v\n", err)
 				return cmdx.FailSilently(cmd)
 			}
-			req = req.PerPage(perPage)
-		}
-
-		identities, _, err := req.Execute()
-		if err != nil {
-			_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the identities: %+v\n", err)
-			return cmdx.FailSilently(cmd)
-		}
 
-		cmdx.PrintTable(cmd, &outputIdentityCollection{
-			identities: identities,
-		})
+			cmdx.PrintTable(cmd, &outputIdentityCollection{
+				identities: identities,
+			})
 
-		return nil
-	},
+			return nil
+		},
+	}
 }
diff --git a/cmd/identities/patch.go b/cmd/identities/patch.go
index 44be38fa1..f9f886bb3 100644
--- a/cmd/identities/patch.go
+++ b/cmd/identities/patch.go
@@ -7,13 +7,15 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var PatchCmd = &cobra.Command{
-	Use:   "patch <file.json [file-2.json [file-3.json] ...]>",
-	Short: "Patch identities by ID (not yet implemented)",
-	Args:  cobra.MinimumNArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		// TODO
-		fmt.Println("not yet implemented")
-		os.Exit(1)
-	},
+func NewPatchCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:   "patch <file.json [file-2.json [file-3.json] ...]>",
+		Short: "Patch identities by ID (not yet implemented)",
+		Args:  cobra.MinimumNArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			// TODO
+			fmt.Println("not yet implemented")
+			os.Exit(1)
+		},
+	}
 }
diff --git a/cmd/identities/root.go b/cmd/identities/root.go
index 2c83d8008..d5c19c44f 100644
--- a/cmd/identities/root.go
+++ b/cmd/identities/root.go
@@ -8,24 +8,26 @@ import (
 	"github.com/ory/kratos/cmd/cliclient"
 )
 
-// identitiesCmd represents the identity command
-var identitiesCmd = &cobra.Command{
-	Use:   "identities",
-	Short: "Tools to interact with remote identities",
+// NewIdentitiesCmd represents the identity command
+func NewIdentitiesCmd() *cobra.Command {
+	var identitiesCmd = &cobra.Command{
+		Use:   "identities",
+		Short: "Tools to interact with remote identities",
+	}
+
+	cliclient.RegisterClientFlags(identitiesCmd.PersistentFlags())
+	cmdx.RegisterFormatFlags(identitiesCmd.PersistentFlags())
+	return identitiesCmd
 }
 
 func RegisterCommandRecursive(parent *cobra.Command) {
-	parent.AddCommand(identitiesCmd)
+	c := NewIdentitiesCmd()
+	parent.AddCommand(c)
 
-	identitiesCmd.AddCommand(ImportCmd)
-	identitiesCmd.AddCommand(ValidateCmd)
-	identitiesCmd.AddCommand(ListCmd)
-	identitiesCmd.AddCommand(GetCmd)
-	identitiesCmd.AddCommand(DeleteCmd)
-	identitiesCmd.AddCommand(PatchCmd)
-}
-
-func RegisterFlags() {
-	cliclient.RegisterClientFlags(identitiesCmd.PersistentFlags())
-	cmdx.RegisterFormatFlags(identitiesCmd.PersistentFlags())
+	c.AddCommand(NewImportCmd())
+	c.AddCommand(NewValidateCmd())
+	c.AddCommand(NewListCmd())
+	c.AddCommand(NewGetCmd())
+	c.AddCommand(NewDeleteCmd())
+	c.AddCommand(NewPatchCmd())
 }
diff --git a/cmd/identities/validate.go b/cmd/identities/validate.go
index 9df205b01..fa2279f03 100644
--- a/cmd/identities/validate.go
+++ b/cmd/identities/validate.go
@@ -21,45 +21,49 @@ import (
 	"github.com/ory/kratos/cmd/cliclient"
 )
 
-var ValidateCmd = &cobra.Command{
-	Use:   "validate <file.json [file-2.json [file-3.json] ...]>",
-	Short: "Validate local identity files",
-	Long: `This command allows validation of identity files.
+func NewValidateCmd() *cobra.Command {
+	var c = &cobra.Command{
+		Use:   "validate <file.json [file-2.json [file-3.json] ...]>",
+		Short: "Validate local identity files",
+		Long: `This command allows validation of identity files.
 It validates against the payload of the API and the identity schema as configured in Ory Kratos.
 Identities can be supplied via STD_IN or JSON files containing a single or an array of identities.
 `,
-	RunE: func(cmd *cobra.Command, args []string) error {
-		c := cliclient.NewClient(cmd)
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c := cliclient.NewClient(cmd)
 
-		is, err := readIdentities(cmd, args)
-		if err != nil {
-			return err
-		}
-
-		for src, i := range is {
-			err = validateIdentity(cmd, src, i, func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error) {
-				return c.PublicApi.GetSchema(ctx, id).Execute()
-			})
+			is, err := readIdentities(cmd, args)
 			if err != nil {
 				return err
 			}
-		}
 
-		_, _ = fmt.Fprintln(cmd.OutOrStdout(), "All identity files are valid.")
-		return nil
-	},
+			for src, i := range is {
+				err = ValidateIdentity(cmd, src, i, func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error) {
+					return c.PublicApi.GetSchema(ctx, id).Execute()
+				})
+				if err != nil {
+					return err
+				}
+			}
+
+			_, _ = fmt.Fprintln(cmd.OutOrStdout(), "All identity files are valid.")
+			return nil
+		},
+	}
+
+	return c
 }
 
 var schemas = make(map[string]*jsonschema.Schema)
 
 const createIdentityPath = "openapi.json#/components/schemas/createIdentity"
 
-type schemaGetter = func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error)
+type SchemaGetter = func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error)
 
-// validateIdentity validates the json payload fc against
+// ValidateIdentity validates the json payload fc against
 // 1. the swagger payload definition and
 // 2. the remote custom identity schema.
-func validateIdentity(cmd *cobra.Command, src, i string, getRemoteSchema schemaGetter) error {
+func ValidateIdentity(cmd *cobra.Command, src, i string, getRemoteSchema SchemaGetter) error {
 	swaggerSchema, ok := schemas[createIdentityPath]
 	if !ok {
 		// add swagger schema
diff --git a/cmd/jsonnet/format.go b/cmd/jsonnet/format.go
index 5b68f011d..14297a718 100644
--- a/cmd/jsonnet/format.go
+++ b/cmd/jsonnet/format.go
@@ -1,18 +1,3 @@
-/*
-Copyright © 2020 NAME HERE <EMAIL ADDRESS>
-
-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 jsonnet
 
 import (
@@ -27,39 +12,38 @@ import (
 	"github.com/ory/x/flagx"
 )
 
-// jsonnetFormatCmd represents the format command
-var jsonnetFormatCmd = &cobra.Command{
-	Use: "format path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]",
-	Long: `Formats JSONNet files using the official JSONNet formatter.
+func NewJsonnetFormatCmd() *cobra.Command {
+	c := &cobra.Command{
+		Use: "format path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]",
+		Long: `Formats JSONNet files using the official JSONNet formatter.
 
 Use -w or --write to write output back to files instead of stdout.
 
 ` + GlobHelp,
-	Args: cobra.MinimumNArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		for _, pattern := range args {
-			files, err := filepath.Glob(pattern)
-			cmdx.Must(err, `Glob path "%s" is not valid: %s`, pattern, err)
-
-			shouldWrite := flagx.MustGetBool(cmd, "write")
-			for _, file := range files {
-				content, err := ioutil.ReadFile(file)
-				cmdx.Must(err, `Unable to read file "%s" because: %s`, file, err)
-
-				output, err := formatter.Format(file, string(content), formatter.DefaultOptions())
-				cmdx.Must(err, `JSONNet file "%s" could not be formatted: %s`, file, err)
-
-				if shouldWrite {
-					err := ioutil.WriteFile(file, []byte(output), 0644) // #nosec
-					cmdx.Must(err, `Could not write to file "%s" because: %s`, file, err)
-				} else {
-					fmt.Println(output)
+		Args: cobra.MinimumNArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			for _, pattern := range args {
+				files, err := filepath.Glob(pattern)
+				cmdx.Must(err, `Glob path "%s" is not valid: %s`, pattern, err)
+
+				shouldWrite := flagx.MustGetBool(cmd, "write")
+				for _, file := range files {
+					content, err := ioutil.ReadFile(file)
+					cmdx.Must(err, `Unable to read file "%s" because: %s`, file, err)
+
+					output, err := formatter.Format(file, string(content), formatter.DefaultOptions())
+					cmdx.Must(err, `JSONNet file "%s" could not be formatted: %s`, file, err)
+
+					if shouldWrite {
+						err := ioutil.WriteFile(file, []byte(output), 0644) // #nosec
+						cmdx.Must(err, `Could not write to file "%s" because: %s`, file, err)
+					} else {
+						fmt.Println(output)
+					}
 				}
 			}
-		}
-	},
-}
-
-func init() {
-	jsonnetFormatCmd.Flags().BoolP("write", "w", false, "Write formatted output back to file.")
+		},
+	}
+	c.Flags().BoolP("write", "w", false, "Write formatted output back to file.")
+	return c
 }
diff --git a/cmd/jsonnet/lint.go b/cmd/jsonnet/lint.go
index 166f0f56d..bb181ae6d 100644
--- a/cmd/jsonnet/lint.go
+++ b/cmd/jsonnet/lint.go
@@ -14,32 +14,33 @@ import (
 	"github.com/ory/x/cmdx"
 )
 
-// jsonnetLintCmd represents the lint command
-var jsonnetLintCmd = &cobra.Command{
-	Use: "lint path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]",
-	Long: `Lints JSONNet files using the official JSONNet linter and exits with a status code of 1 when issues are detected.
+func NewJsonnetLintCmd() *cobra.Command {
+	return &cobra.Command{
+		Use: "lint path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]",
+		Long: `Lints JSONNet files using the official JSONNet linter and exits with a status code of 1 when issues are detected.
 
 ` + GlobHelp,
-	Args: cobra.MinimumNArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		vm := jsonnet.MakeVM()
+		Args: cobra.MinimumNArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			vm := jsonnet.MakeVM()
 
-		for _, pattern := range args {
-			files, err := filepath.Glob(pattern)
-			cmdx.Must(err, `Glob path "%s" is not valid: %s`, pattern, err)
+			for _, pattern := range args {
+				files, err := filepath.Glob(pattern)
+				cmdx.Must(err, `Glob path "%s" is not valid: %s`, pattern, err)
 
-			for _, file := range files {
-				content, err := ioutil.ReadFile(file)
-				cmdx.Must(err, `Unable to read file "%s" because: %s`, file, err)
+				for _, file := range files {
+					content, err := ioutil.ReadFile(file)
+					cmdx.Must(err, `Unable to read file "%s" because: %s`, file, err)
 
-				var outBuilder strings.Builder
-				errorsFound := linter.LintSnippet(vm, &outBuilder, file, string(content))
+					var outBuilder strings.Builder
+					errorsFound := linter.LintSnippet(vm, &outBuilder, file, string(content))
 
-				if errorsFound {
-					_, _ = fmt.Fprintf(os.Stderr, "Linter found issues.")
-					os.Exit(1)
+					if errorsFound {
+						_, _ = fmt.Fprintf(os.Stderr, "Linter found issues.")
+						os.Exit(1)
+					}
 				}
 			}
-		}
-	},
+		},
+	}
 }
diff --git a/cmd/jsonnet/root.go b/cmd/jsonnet/root.go
index 06eb6827a..62442ee6b 100644
--- a/cmd/jsonnet/root.go
+++ b/cmd/jsonnet/root.go
@@ -29,15 +29,19 @@ const GlobHelp = `Glob Syntax:
         pattern { ',' pattern }
                     comma-separated (without spaces) patterns`
 
-// jsonnetCmd represents the jsonnet command
-var jsonnetCmd = &cobra.Command{
-	Use:   "jsonnet",
-	Short: "Helpers for linting and formatting JSONNet code",
+func NewJsonnetCmd() *cobra.Command {
+	c := &cobra.Command{
+		Use:   "jsonnet",
+		Short: "Helpers for linting and formatting JSONNet code",
+	}
+
+	return c
 }
 
 func RegisterCommandRecursive(parent *cobra.Command) {
-	parent.AddCommand(jsonnetCmd)
+	c := NewJsonnetCmd()
+	parent.AddCommand(c)
 
-	jsonnetCmd.AddCommand(jsonnetFormatCmd)
-	jsonnetCmd.AddCommand(jsonnetLintCmd)
+	c.AddCommand(NewJsonnetFormatCmd())
+	c.AddCommand(NewJsonnetLintCmd())
 }
diff --git a/cmd/root.go b/cmd/root.go
index 02c6177bb..5172f86be 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -22,31 +22,33 @@ import (
 )
 
 // RootCmd represents the base command when called without any subcommands
-var RootCmd = &cobra.Command{
-	Use: "kratos",
+func NewRootCmd() (cmd *cobra.Command) {
+	cmd = &cobra.Command{
+		Use: "kratos",
+	}
+	identities.RegisterCommandRecursive(cmd)
+
+	jsonnet.RegisterCommandRecursive(cmd)
+	serve.RegisterCommandRecursive(cmd)
+	migrate.RegisterCommandRecursive(cmd)
+	remote.RegisterCommandRecursive(cmd)
+	hashers.RegisterCommandRecursive(cmd)
+	courier.RegisterCommandRecursive(cmd)
+
+	cmd.AddCommand(cmdx.Version(&config.Version, &config.Commit, &config.Date))
+
+	return cmd
 }
 
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the RootCmd.
 func Execute() {
-	if err := RootCmd.Execute(); err != nil {
+	c := NewRootCmd()
+
+	if err := c.Execute(); err != nil {
 		if !errors.Is(err, cmdx.ErrNoPrintButFail) {
-			_, _ = fmt.Fprintln(RootCmd.ErrOrStderr(), err)
+			_, _ = fmt.Fprintln(c.ErrOrStderr(), err)
 		}
 		os.Exit(1)
 	}
 }
-
-func init() {
-	identities.RegisterCommandRecursive(RootCmd)
-	identities.RegisterFlags()
-
-	jsonnet.RegisterCommandRecursive(RootCmd)
-	serve.RegisterCommandRecursive(RootCmd)
-	migrate.RegisterCommandRecursive(RootCmd)
-	remote.RegisterCommandRecursive(RootCmd)
-	hashers.RegisterCommandRecursive(RootCmd)
-	courier.RegisterCommandRecursive(RootCmd)
-
-	RootCmd.AddCommand(cmdx.Version(&config.Version, &config.Commit, &config.Date))
-}
diff --git a/cmd/serve/root.go b/cmd/serve/root.go
index d3ee01903..e05899a51 100644
--- a/cmd/serve/root.go
+++ b/cmd/serve/root.go
@@ -25,42 +25,42 @@ import (
 )
 
 // serveCmd represents the serve command
-var serveCmd = &cobra.Command{
-	Use:   "serve",
-	Short: "Run the Ory Kratos server",
-	Run: func(cmd *cobra.Command, args []string) {
-		d := driver.New(cmd.Context(), configx.WithFlags(cmd.Flags()))
+func NewServeCmd() (serveCmd *cobra.Command) {
+	serveCmd = &cobra.Command{
+		Use:   "serve",
+		Short: "Run the Ory Kratos server",
+		Run: func(cmd *cobra.Command, args []string) {
+			d := driver.New(cmd.Context(), configx.WithFlags(cmd.Flags()))
 
-		if d.Config(cmd.Context()).IsInsecureDevMode() {
-			d.Logger().Warn(`
+			if d.Config(cmd.Context()).IsInsecureDevMode() {
+				d.Logger().Warn(`
 
 YOU ARE RUNNING Ory KRATOS IN DEV MODE.
 SECURITY IS DISABLED.
 DON'T DO THIS IN PRODUCTION!
 
 `)
-		}
+			}
 
-		configVersion := d.Config(cmd.Context()).ConfigVersion()
-		if configVersion == config.UnknownVersion {
-			d.Logger().Warn("The config has no version specified. Add the version to improve your development experience.")
-		} else if config.Version != "" &&
-			configVersion != config.Version {
-			d.Logger().Warnf("Config version is '%s' but kratos runs on version '%s'", configVersion, config.Version)
-		}
+			configVersion := d.Config(cmd.Context()).ConfigVersion()
+			if configVersion == config.UnknownVersion {
+				d.Logger().Warn("The config has no version specified. Add the version to improve your development experience.")
+			} else if config.Version != "" &&
+				configVersion != config.Version {
+				d.Logger().Warnf("Config version is '%s' but kratos runs on version '%s'", configVersion, config.Version)
+			}
 
-		daemon.ServeAll(d)(cmd, args)
-	},
-}
-
-func RegisterCommandRecursive(parent *cobra.Command) {
-	parent.AddCommand(serveCmd)
-}
-
-func init() {
+			daemon.ServeAll(d)(cmd, args)
+		},
+	}
 	configx.RegisterFlags(serveCmd.PersistentFlags())
 
 	serveCmd.PersistentFlags().Bool("sqa-opt-out", false, "Disable anonymized telemetry reports - for more information please visit https://www.ory.sh/docs/ecosystem/sqa")
 	serveCmd.PersistentFlags().Bool("dev", false, "Disables critical security features to make development easier")
 	serveCmd.PersistentFlags().Bool("watch-courier", false, "Run the message courier as a background task, to simplify single-instance setup")
+	return serveCmd
+}
+
+func RegisterCommandRecursive(parent *cobra.Command) {
+	parent.AddCommand(NewServeCmd())
 }
diff --git a/docs/docs/cli/kratos.md b/docs/docs/cli/kratos.md
index 5758d087f..796bee9d5 100644
--- a/docs/docs/cli/kratos.md
+++ b/docs/docs/cli/kratos.md
@@ -22,8 +22,6 @@ To improve this file please make your change against the appropriate "./cmd/*.go
 
 - [kratos courier](kratos-courier) - Commands related to the Ory Kratos message
   courier
-- [kratos hashers](kratos-hashers) - This command contains helpers around
-  hashing
 - [kratos identities](kratos-identities) - Tools to interact with remote
   identities
 - [kratos jsonnet](kratos-jsonnet) - Helpers for linting and formatting JSONNet
diff --git a/docs/sidebar.json b/docs/sidebar.json
index 7a9a01bdd..aaa9e25e6 100644
--- a/docs/sidebar.json
+++ b/docs/sidebar.json
@@ -78,11 +78,6 @@
           "cli/kratos",
           "cli/kratos-courier",
           "cli/kratos-courier-watch",
-          "cli/kratos-hashers",
-          "cli/kratos-hashers-argon2",
-          "cli/kratos-hashers-argon2-calibrate",
-          "cli/kratos-hashers-argon2-hash",
-          "cli/kratos-hashers-argon2-load-test",
           "cli/kratos-identities",
           "cli/kratos-identities-delete",
           "cli/kratos-identities-get",
diff --git a/go.mod b/go.mod
index f11853273..9d71db88e 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,7 @@ require (
 	github.com/HdrHistogram/hdrhistogram-go v1.1.0 // indirect
 	github.com/Masterminds/sprig/v3 v3.0.0
 	github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0
+	github.com/avast/retry-go/v3 v3.1.1
 	github.com/bwmarrin/discordgo v0.23.0
 	github.com/bxcodec/faker/v3 v3.3.1
 	github.com/cenkalti/backoff v2.2.1+incompatible
diff --git a/go.sum b/go.sum
index 516203535..e22806b5e 100644
--- a/go.sum
+++ b/go.sum
@@ -127,6 +127,8 @@ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:o
 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
 github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
 github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
+github.com/avast/retry-go/v3 v3.1.1 h1:49Scxf4v8PmiQ/nY0aY3p0hDueqSmc7++cBbtiDGu2g=
+github.com/avast/retry-go/v3 v3.1.1/go.mod h1:6cXRK369RpzFL3UQGqIUp9Q7GDrams+KsYWrfNA1/nQ=
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
 github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=


Test patch:
diff --git a/cmd/identities/delete_test.go b/cmd/identities/delete_test.go
index 8103948d7..ee0791729 100644
--- a/cmd/identities/delete_test.go
+++ b/cmd/identities/delete_test.go
@@ -1,4 +1,4 @@
-package identities
+package identities_test
 
 import (
 	"context"
@@ -6,6 +6,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/ory/kratos/cmd/identities"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -16,14 +18,15 @@ import (
 )
 
 func TestDeleteCmd(t *testing.T) {
-	reg := setup(t, DeleteCmd)
+	c := identities.NewDeleteCmd()
+	reg := setup(t, c)
 
 	t.Run("case=deletes successfully", func(t *testing.T) {
 		// create identity to delete
 		i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
 		require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
 
-		stdOut := execNoErr(t, DeleteCmd, i.ID.String())
+		stdOut := execNoErr(t, c, i.ID.String())
 
 		// expect ID and no error
 		assert.Equal(t, i.ID.String()+"\n", stdOut)
@@ -36,7 +39,7 @@ func TestDeleteCmd(t *testing.T) {
 	t.Run("case=deletes three identities", func(t *testing.T) {
 		is, ids := makeIdentities(t, reg, 3)
 
-		stdOut := execNoErr(t, DeleteCmd, ids...)
+		stdOut := execNoErr(t, c, ids...)
 
 		assert.Equal(t, strings.Join(ids, "\n")+"\n", stdOut)
 
@@ -47,7 +50,7 @@ func TestDeleteCmd(t *testing.T) {
 	})
 
 	t.Run("case=fails with unknown ID", func(t *testing.T) {
-		stdErr := execErr(t, DeleteCmd, x.NewUUID().String())
+		stdErr := execErr(t, c, x.NewUUID().String())
 
 		assert.Contains(t, stdErr, "404 Not Found", stdErr)
 	})
diff --git a/cmd/identities/get_test.go b/cmd/identities/get_test.go
index 2699d7e61..636951715 100644
--- a/cmd/identities/get_test.go
+++ b/cmd/identities/get_test.go
@@ -1,10 +1,12 @@
-package identities
+package identities_test
 
 import (
 	"context"
 	"encoding/json"
 	"testing"
 
+	"github.com/ory/kratos/cmd/identities"
+
 	"github.com/ory/x/assertx"
 
 	"github.com/ory/kratos/x"
@@ -17,13 +19,14 @@ import (
 )
 
 func TestGetCmd(t *testing.T) {
-	reg := setup(t, GetCmd)
+	c := identities.NewGetCmd()
+	reg := setup(t, c)
 
 	t.Run("case=gets a single identity", func(t *testing.T) {
 		i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
 		require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
 
-		stdOut := execNoErr(t, GetCmd, i.ID.String())
+		stdOut := execNoErr(t, c, i.ID.String())
 
 		ij, err := json.Marshal(i)
 		require.NoError(t, err)
@@ -34,7 +37,7 @@ func TestGetCmd(t *testing.T) {
 	t.Run("case=gets three identities", func(t *testing.T) {
 		is, ids := makeIdentities(t, reg, 3)
 
-		stdOut := execNoErr(t, GetCmd, ids...)
+		stdOut := execNoErr(t, c, ids...)
 
 		isj, err := json.Marshal(is)
 		require.NoError(t, err)
@@ -43,7 +46,7 @@ func TestGetCmd(t *testing.T) {
 	})
 
 	t.Run("case=fails with unknown ID", func(t *testing.T) {
-		stdErr := execErr(t, GetCmd, x.NewUUID().String())
+		stdErr := execErr(t, c, x.NewUUID().String())
 
 		assert.Contains(t, stdErr, "404 Not Found", stdErr)
 	})
diff --git a/cmd/identities/helpers_test.go b/cmd/identities/helpers_test.go
new file mode 100644
index 000000000..1cc390529
--- /dev/null
+++ b/cmd/identities/helpers_test.go
@@ -0,0 +1,73 @@
+package identities_test
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"testing"
+
+	"github.com/ory/x/cmdx"
+
+	"github.com/pkg/errors"
+
+	"github.com/ory/kratos/identity"
+
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ory/kratos/cmd/cliclient"
+	"github.com/ory/kratos/driver"
+	"github.com/ory/kratos/driver/config"
+	"github.com/ory/kratos/internal"
+	"github.com/ory/kratos/internal/testhelpers"
+)
+
+func setup(t *testing.T, cmd *cobra.Command) driver.Registry {
+	conf, reg := internal.NewFastRegistryWithMocks(t)
+	_, admin := testhelpers.NewKratosServerWithCSRF(t, reg)
+	conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stubs/identity.schema.json")
+	// setup command
+	cliclient.RegisterClientFlags(cmd.Flags())
+	cmdx.RegisterFormatFlags(cmd.Flags())
+	require.NoError(t, cmd.Flags().Set(cliclient.FlagEndpoint, admin.URL))
+	require.NoError(t, cmd.Flags().Set(cmdx.FlagFormat, string(cmdx.FormatJSON)))
+	return reg
+}
+
+func exec(cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) {
+	stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{}
+	cmd.SetErr(stdErr)
+	cmd.SetOut(stdOut)
+	cmd.SetIn(stdIn)
+	defer cmd.SetIn(nil)
+	if args == nil {
+		args = []string{}
+	}
+	cmd.SetArgs(args)
+	err := cmd.Execute()
+	return stdOut.String(), stdErr.String(), err
+}
+
+func execNoErr(t *testing.T, cmd *cobra.Command, args ...string) string {
+	stdOut, stdErr, err := exec(cmd, nil, args...)
+	require.NoError(t, err)
+	require.Len(t, stdErr, 0, stdOut)
+	return stdOut
+}
+
+func execErr(t *testing.T, cmd *cobra.Command, args ...string) string {
+	stdOut, stdErr, err := exec(cmd, nil, args...)
+	require.True(t, errors.Is(err, cmdx.ErrNoPrintButFail))
+	require.Len(t, stdOut, 0, stdErr)
+	return stdErr
+}
+
+func makeIdentities(t *testing.T, reg driver.Registry, n int) (is []*identity.Identity, ids []string) {
+	for j := 0; j < n; j++ {
+		i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
+		require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
+		is = append(is, i)
+		ids = append(ids, i.ID.String())
+	}
+	return
+}
diff --git a/cmd/identities/import_test.go b/cmd/identities/import_test.go
index c19bdf44c..e512c3cc0 100644
--- a/cmd/identities/import_test.go
+++ b/cmd/identities/import_test.go
@@ -1,4 +1,4 @@
-package identities
+package identities_test
 
 import (
 	"bytes"
@@ -8,6 +8,8 @@ import (
 	"io/ioutil"
 	"testing"
 
+	"github.com/ory/kratos/cmd/identities"
+
 	"github.com/gofrs/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -19,7 +21,8 @@ import (
 )
 
 func TestImportCmd(t *testing.T) {
-	reg := setup(t, ImportCmd)
+	c := identities.NewImportCmd()
+	reg := setup(t, c)
 
 	t.Run("case=imports a new identity from file", func(t *testing.T) {
 		i := kratos.CreateIdentity{
@@ -34,7 +37,7 @@ func TestImportCmd(t *testing.T) {
 		require.NoError(t, err)
 		require.NoError(t, f.Close())
 
-		stdOut := execNoErr(t, ImportCmd, f.Name())
+		stdOut := execNoErr(t, c, f.Name())
 
 		id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
 		require.NoError(t, err)
@@ -61,7 +64,7 @@ func TestImportCmd(t *testing.T) {
 		require.NoError(t, err)
 		require.NoError(t, f.Close())
 
-		stdOut := execNoErr(t, ImportCmd, f.Name())
+		stdOut := execNoErr(t, c, f.Name())
 
 		id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String())
 		require.NoError(t, err)
@@ -88,7 +91,7 @@ func TestImportCmd(t *testing.T) {
 		ij, err := json.Marshal(i)
 		require.NoError(t, err)
 
-		stdOut, stdErr, err := exec(ImportCmd, bytes.NewBuffer(ij))
+		stdOut, stdErr, err := exec(c, bytes.NewBuffer(ij))
 		require.NoError(t, err, "%s %s", stdOut, stdErr)
 
 		id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String())
@@ -110,7 +113,7 @@ func TestImportCmd(t *testing.T) {
 		ij, err := json.Marshal(i)
 		require.NoError(t, err)
 
-		stdOut, stdErr, err := exec(ImportCmd, bytes.NewBuffer(ij))
+		stdOut, stdErr, err := exec(c, bytes.NewBuffer(ij))
 		require.NoError(t, err, "%s %s", stdOut, stdErr)
 
 		id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
@@ -121,7 +124,7 @@ func TestImportCmd(t *testing.T) {
 
 	t.Run("case=fails to import invalid identity", func(t *testing.T) {
 		// validation is further tested with the validate command
-		stdOut, stdErr, err := exec(ImportCmd, bytes.NewBufferString("{}"))
+		stdOut, stdErr, err := exec(c, bytes.NewBufferString("{}"))
 		assert.True(t, errors.Is(err, cmdx.ErrNoPrintButFail))
 		assert.Contains(t, stdErr, "STD_IN[0]: not valid")
 		assert.Len(t, stdOut, 0)
diff --git a/cmd/identities/list_test.go b/cmd/identities/list_test.go
index c1bffddd8..86a81782f 100644
--- a/cmd/identities/list_test.go
+++ b/cmd/identities/list_test.go
@@ -1,10 +1,12 @@
-package identities
+package identities_test
 
 import (
 	"context"
 	"strings"
 	"testing"
 
+	"github.com/ory/kratos/cmd/identities"
+
 	"github.com/ory/x/cmdx"
 
 	"github.com/stretchr/testify/assert"
@@ -14,8 +16,9 @@ import (
 )
 
 func TestListCmd(t *testing.T) {
-	reg := setup(t, ListCmd)
-	require.NoError(t, ListCmd.Flags().Set(cmdx.FlagQuiet, "true"))
+	c := identities.NewListCmd()
+	reg := setup(t, c)
+	require.NoError(t, c.Flags().Set(cmdx.FlagQuiet, "true"))
 
 	var deleteIdentities = func(t *testing.T, is []*identity.Identity) {
 		for _, i := range is {
@@ -27,7 +30,7 @@ func TestListCmd(t *testing.T) {
 		is, ids := makeIdentities(t, reg, 5)
 		defer deleteIdentities(t, is)
 
-		stdOut := execNoErr(t, ListCmd)
+		stdOut := execNoErr(t, c)
 
 		for _, i := range ids {
 			assert.Contains(t, stdOut, i)
@@ -38,8 +41,8 @@ func TestListCmd(t *testing.T) {
 		is, ids := makeIdentities(t, reg, 6)
 		defer deleteIdentities(t, is)
 
-		stdoutP1 := execNoErr(t, ListCmd, "1", "3")
-		stdoutP2 := execNoErr(t, ListCmd, "2", "3")
+		stdoutP1 := execNoErr(t, c, "1", "3")
+		stdoutP2 := execNoErr(t, c, "2", "3")
 
 		for _, id := range ids {
 			// exactly one of page 1 and 2 should contain the id
diff --git a/cmd/identities/validate_test.go b/cmd/identities/validate_test.go
index a36926a7c..02a301519 100644
--- a/cmd/identities/validate_test.go
+++ b/cmd/identities/validate_test.go
@@ -1,4 +1,4 @@
-package identities
+package identities_test
 
 import (
 	"bytes"
@@ -7,6 +7,8 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/ory/kratos/cmd/identities"
+
 	"github.com/spf13/cobra"
 	"github.com/stretchr/testify/assert"
 )
@@ -20,7 +22,7 @@ func TestValidateIdentity(t *testing.T) {
 		return cmd, out, err
 	}
 
-	var testSchemaGetter = func(resp map[string]interface{}) schemaGetter {
+	var testSchemaGetter = func(resp map[string]interface{}) identities.SchemaGetter {
 		return func(_ context.Context, _ string) (map[string]interface{}, *http.Response, error) {
 			return resp, nil, nil
 		}
@@ -63,7 +65,7 @@ func TestValidateIdentity(t *testing.T) {
 			if tc.identitySchema == nil {
 				tc.identitySchema = map[string]interface{}{}
 			}
-			err := validateIdentity(cmd, "test identity", tc.payload, testSchemaGetter(tc.identitySchema))
+			err := identities.ValidateIdentity(cmd, "test identity", tc.payload, testSchemaGetter(tc.identitySchema))
 			assert.Error(t, err, stdOut.String(), stdErr.String())
 			assert.Len(t, stdOut.String(), 0, stdErr.String())
 			assert.Contains(t, stdErr.String(), "required", stdOut.String())
diff --git a/cmd/serve/root_test.go b/cmd/serve/root_test.go
new file mode 100644
index 000000000..1ceac19e4
--- /dev/null
+++ b/cmd/serve/root_test.go
@@ -0,0 +1,11 @@
+package serve_test
+
+import (
+	"testing"
+
+	"github.com/ory/kratos/internal/testhelpers"
+)
+
+func TestServe(t *testing.T) {
+	_, _ = testhelpers.StartE2EServer(t, "../../contrib/quickstart/kratos/email-password/kratos.yml")
+}
diff --git a/examples/go/init/login/main_test.go b/examples/go/init/login/main_test.go
index b979d161f..5ed4b2685 100644
--- a/examples/go/init/login/main_test.go
+++ b/examples/go/init/login/main_test.go
@@ -3,13 +3,15 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 
 	"github.com/stretchr/testify/require"
 )
 
 func TestLogin(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 
 	flow := initLogin()
diff --git a/examples/go/init/recovery/main_test.go b/examples/go/init/recovery/main_test.go
index 828e84fed..499693561 100644
--- a/examples/go/init/recovery/main_test.go
+++ b/examples/go/init/recovery/main_test.go
@@ -3,13 +3,15 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 
 	"github.com/stretchr/testify/require"
 )
 
 func TestRecovery(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 
 	flow := initRecovery()
diff --git a/examples/go/init/registration/main_test.go b/examples/go/init/registration/main_test.go
index 301379b8a..877d70754 100644
--- a/examples/go/init/registration/main_test.go
+++ b/examples/go/init/registration/main_test.go
@@ -3,13 +3,15 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 
 	"github.com/stretchr/testify/require"
 )
 
 func TestRegistration(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 
 	flow := initRegistration()
diff --git a/examples/go/init/settings/main_test.go b/examples/go/init/settings/main_test.go
index 91c6de117..2628bd617 100644
--- a/examples/go/init/settings/main_test.go
+++ b/examples/go/init/settings/main_test.go
@@ -3,13 +3,15 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 
 	"github.com/stretchr/testify/require"
 )
 
 func TestSettings(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 
 	flow := initSettings()
diff --git a/examples/go/init/verification/main_test.go b/examples/go/init/verification/main_test.go
index def9a39a5..d5d00acc9 100644
--- a/examples/go/init/verification/main_test.go
+++ b/examples/go/init/verification/main_test.go
@@ -3,13 +3,15 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 
 	"github.com/stretchr/testify/require"
 )
 
 func TestVerification(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 
 	flow := initVerification()
diff --git a/examples/go/pkg/tests.go b/examples/go/pkg/tests.go
deleted file mode 100644
index a78d2e77d..000000000
--- a/examples/go/pkg/tests.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package pkg
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	"github.com/phayes/freeport"
-	"github.com/spf13/cobra"
-	"github.com/stretchr/testify/require"
-
-	"github.com/ory/kratos/cmd"
-	"github.com/ory/x/cmdx"
-	"github.com/ory/x/configx"
-)
-
-func NewKratosServer(t testing.TB) (publicURL, adminURL string) {
-	dsn := "sqlite://file::memory:?_fk=true&cache=shared"
-
-	publicPort, err := freeport.GetFreePort()
-	require.NoError(t, err)
-
-	adminPort, err := freeport.GetFreePort()
-	require.NoError(t, err)
-
-	ctx := configx.ContextWithConfigOptions(context.Background(),
-		configx.WithValue("dsn", dsn),
-		configx.WithValue("dev", true),
-		configx.WithValue("log.level", "trace"),
-		configx.WithValue("serve.public.port", publicPort),
-		configx.WithValue("serve.admin.port", adminPort),
-	)
-	ctx, cancel := context.WithCancel(ctx)
-	t.Cleanup(cancel)
-
-	executor := &cmdx.CommandExecuter{
-		New: func() *cobra.Command {
-			return cmd.RootCmd
-		},
-		Ctx: ctx,
-	}
-	_ = executor.ExecNoErr(t, "")
-
-	go func() {
-		t.Log("Starting server...")
-		_ = executor.ExecNoErr(t, "serve", "--config", "../../pkg/stub/kratos.yaml")
-	}()
-
-	publicURL = fmt.Sprintf("http://127.0.0.1:%d", publicPort)
-	adminURL = fmt.Sprintf("http://127.0.0.1:%d", adminPort)
-
-	startedAt := time.Now()
-	for {
-		_, err := http.Get(fmt.Sprintf("%s/health/alive", publicURL))
-		if err == nil {
-			break
-		}
-
-		time.Sleep(50 * time.Millisecond)
-		if time.Now().Sub(startedAt).Seconds() > 5 {
-			t.Fatalf("Unable to reach server.")
-		}
-	}
-
-	return
-}
diff --git a/examples/go/submit/logout/main_test.go b/examples/go/submit/logout/main_test.go
index 6d42cd74f..dced37e4f 100644
--- a/examples/go/submit/logout/main_test.go
+++ b/examples/go/submit/logout/main_test.go
@@ -3,11 +3,13 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 )
 
 func TestLogout(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 
 	performLogout()
diff --git a/examples/go/submit/registration/main_test.go b/examples/go/submit/registration/main_test.go
index 972eba603..f9c19012e 100644
--- a/examples/go/submit/registration/main_test.go
+++ b/examples/go/submit/registration/main_test.go
@@ -3,11 +3,13 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 )
 
 func TestRegistration(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 	performRegistration()
 }
diff --git a/examples/go/submit/verification/main_test.go b/examples/go/submit/verification/main_test.go
index 0e0db2dda..d4045b670 100644
--- a/examples/go/submit/verification/main_test.go
+++ b/examples/go/submit/verification/main_test.go
@@ -3,6 +3,8 @@ package main
 import (
 	"testing"
 
+	"github.com/ory/kratos/internal/testhelpers"
+
 	"github.com/ory/kratos/examples/go/pkg"
 
 	"github.com/google/uuid"
@@ -11,7 +13,7 @@ import (
 )
 
 func TestVerification(t *testing.T) {
-	publicURL, _ := pkg.NewKratosServer(t)
+	publicURL, _ := testhelpers.StartE2EServer(t, "../../pkg/stub/kratos.yaml")
 	client = pkg.NewSDKForSelfHosted(publicURL)
 
 	flow := performVerification("dev+" + uuid.New().String() + "@ory.sh")
diff --git a/internal/testhelpers/e2e_server.go b/internal/testhelpers/e2e_server.go
new file mode 100644
index 000000000..d61a0e831
--- /dev/null
+++ b/internal/testhelpers/e2e_server.go
@@ -0,0 +1,68 @@
+package testhelpers
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/avast/retry-go/v3"
+	"github.com/phayes/freeport"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ory/kratos/cmd"
+	"github.com/ory/kratos/x"
+	"github.com/ory/x/cmdx"
+	"github.com/ory/x/configx"
+)
+
+func StartE2EServer(t *testing.T, configFile string) (publicUrl, adminUrl string) {
+	adminPort, err := freeport.GetFreePort()
+	require.NoError(t, err)
+
+	publicPort, err := freeport.GetFreePort()
+	require.NoError(t, err)
+
+	ctx := configx.ContextWithConfigOptions(context.Background(),
+		configx.WithValue("dsn", "memory"),
+		configx.WithValue("dev", true),
+		configx.WithValue("log.level", "trace"),
+		configx.WithValue("log.leak_sensitive_values", true),
+		configx.WithValue("serve.public.port", publicPort),
+		configx.WithValue("serve.admin.port", adminPort),
+	)
+
+	//nolint:staticcheck
+	ctx = context.WithValue(ctx, "dsn", "memory")
+	ctx, cancel := context.WithCancel(ctx)
+	t.Cleanup(cancel)
+
+	executor := &cmdx.CommandExecuter{New: func() *cobra.Command {
+		return cmd.NewRootCmd()
+	}, Ctx: ctx}
+
+	go func() {
+		t.Log("Starting server...")
+		_ = executor.ExecNoErr(t, "serve", "--config", configFile, "--watch-courier")
+	}()
+
+	publicUrl = fmt.Sprintf("http://127.0.0.1:%d", publicPort)
+	adminUrl = fmt.Sprintf("http://127.0.0.1:%d", adminPort)
+
+	require.NoError(t, retry.Do(func() error {
+		res, err := http.Get(publicUrl + "/health/alive")
+		if err != nil {
+			return err
+		}
+		defer res.Body.Close()
+		body := x.MustReadAll(res.Body)
+		if res.StatusCode != http.StatusOK {
+			t.Logf("%s", body)
+			return fmt.Errorf("expected status code 200 but got: %d", res.StatusCode)
+		}
+		return nil
+	}), err)
+
+	return
+}