[];
// Type $$Props to narrow the handler function based on wether this is an update or a new app
@@ -167,19 +172,35 @@
production |
- {prodScheme}{appName}.{prodUrl}
+
|
- {prodScheme}dashboard.{appName}.{prodUrl}
+
|
staging |
- {stagingScheme}{appName}-staging.{stagingUrl}
+
|
- {stagingScheme}dashboard.{appName}-staging.{stagingUrl}
+
|
@@ -223,7 +244,7 @@
{@html l.translate('app.environment.target.changed.description', [
- initialData.production.target.url
+ initialData.production.target.name
])}
@@ -252,7 +273,7 @@
{@html l.translate('app.environment.target.changed.description', [
- initialData.production.target.url
+ initialData.production.target.name
])}
diff --git a/cmd/serve/front/src/routes/(main)/apps/service-url.svelte b/cmd/serve/front/src/routes/(main)/apps/service-url.svelte
new file mode 100644
index 00000000..96419b33
--- /dev/null
+++ b/cmd/serve/front/src/routes/(main)/apps/service-url.svelte
@@ -0,0 +1,15 @@
+
+
+{#if scheme}
+ {scheme}{prefix}{appName}{suffix}.{host}
+{:else}
+ - ({l.translate('target.manual_proxy')})
+{/if}
diff --git a/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte b/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte
index 844e8a06..8cae3108 100644
--- a/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte
+++ b/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte
@@ -14,7 +14,7 @@
export let data;
const submit = (d: UpdateTarget) =>
- service.update(data.target.id, d).then((t) => goto(routes.targets));
+ service.update(data.target.id, d).then(() => goto(routes.targets));
const {
loading: deleting,
diff --git a/cmd/serve/front/src/routes/(main)/targets/target-form.svelte b/cmd/serve/front/src/routes/(main)/targets/target-form.svelte
index b12201ba..bc9b4855 100644
--- a/cmd/serve/front/src/routes/(main)/targets/target-form.svelte
+++ b/cmd/serve/front/src/routes/(main)/targets/target-form.svelte
@@ -26,7 +26,8 @@
let name = initialData?.name ?? '';
let url = initialData?.url ?? '';
let provider: ProviderTypes = initialData?.provider.kind ?? providerTypes[0];
- let isRemote = !!initialData?.provider.data.host ?? false;
+ let isRemote = !!initialData?.provider.data.host || false;
+ let automaticProxyConfiguration = initialData ? !!initialData.url : true;
const docker = { ...initialData?.provider.data };
@@ -48,7 +49,7 @@
if (!initialData) {
formData = {
name,
- url,
+ url: automaticProxyConfiguration ? url : undefined,
docker:
provider === 'docker'
? isRemote
@@ -64,7 +65,7 @@
} else {
formData = {
name,
- url,
+ url: automaticProxyConfiguration ? (initialData?.url !== url ? url : undefined) : null,
docker:
provider === 'docker'
? isRemote
@@ -114,9 +115,17 @@
{l.translate('target.name.help')}
-
- {@html l.translate('target.url.help')}
-
+
+ {@html l.translate('target.automatic_proxy_configuration.help')}
+
+ {#if automaticProxyConfiguration}
+
+ {@html l.translate('target.url.help')}
+
+ {/if}
diff --git a/cmd/serve/healthcheck.go b/cmd/serve/healthcheck.go
index 2ef90f9c..94440a17 100644
--- a/cmd/serve/healthcheck.go
+++ b/cmd/serve/healthcheck.go
@@ -11,7 +11,7 @@ type healthCheckResponse struct {
}
func (s *server) healthcheckHandler(ctx *gin.Context) {
- http.Ok(ctx, healthCheckResponse{
+ _ = http.Ok(ctx, healthCheckResponse{
Version: version.Current(),
})
}
diff --git a/cmd/serve/middlewares.go b/cmd/serve/middlewares.go
index a2b59313..3e0b4a48 100644
--- a/cmd/serve/middlewares.go
+++ b/cmd/serve/middlewares.go
@@ -31,7 +31,7 @@ func (s *server) authenticate(withApiAccess bool) gin.HandlerFunc {
// If it failed and api access is not allowed, return early
if failed && !withApiAccess {
- ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized)
+ _ = ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized)
return
}
@@ -46,14 +46,14 @@ func (s *server) authenticate(withApiAccess bool) gin.HandlerFunc {
authHeader := ctx.GetHeader(apiAuthHeader)
if !strings.HasPrefix(authHeader, apiAuthPrefix) {
- ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized)
+ _ = ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized)
return
}
id, err := s.usersReader.GetIDFromAPIKey(ctx.Request.Context(), domain.APIKey(authHeader[apiAuthPrefixLength:]))
if err != nil {
- ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized)
+ _ = ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized)
return
}
@@ -66,8 +66,8 @@ func (s *server) authenticate(withApiAccess bool) gin.HandlerFunc {
func (s *server) requestLogger(ctx *gin.Context) {
defer func(start time.Time, c *gin.Context) {
- path := ctx.Request.URL.Path
- raw := ctx.Request.URL.RawQuery
+ path := c.Request.URL.Path
+ raw := c.Request.URL.RawQuery
if raw != "" {
path = path + "?" + raw
diff --git a/cmd/serve/server.go b/cmd/serve/server.go
index 6368a91e..d1b18f63 100644
--- a/cmd/serve/server.go
+++ b/cmd/serve/server.go
@@ -34,6 +34,7 @@ type (
ServerOptions interface {
Secret() []byte
IsSecure() bool
+ IsDebug() bool
ListenAddress() string
}
@@ -59,17 +60,21 @@ func newHttpServer(options ServerOptions, root startup.ServerRoot) *server {
logger: root.Logger(),
}
- s.router.SetTrustedProxies(nil)
+ _ = s.router.SetTrustedProxies(nil)
// Configure the session store
- store := cookie.NewStore(s.options.Secret())
+ store := cookie.NewStore(options.Secret())
store.Options(sessions.Options{
- Secure: s.options.IsSecure(),
+ Secure: options.IsSecure(),
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
- s.router.Use(s.requestLogger, s.recoverer, sessions.Sessions(sessionName, store))
+ if options.IsDebug() {
+ s.router.Use(s.requestLogger)
+ }
+
+ s.router.Use(s.recoverer, sessions.Sessions(sessionName, store))
// Let's register every routes now!
v1 := s.router.Group("/api/v1")
diff --git a/cmd/startup/server.go b/cmd/startup/server.go
index bbf7fe29..723ce15b 100644
--- a/cmd/startup/server.go
+++ b/cmd/startup/server.go
@@ -47,7 +47,6 @@ type (
}
serverRoot struct {
- options ServerOptions
bus bus.Bus
logger log.Logger
db *sqlite.Database
@@ -61,13 +60,15 @@ type (
// needed by the server.
func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) {
s := &serverRoot{
- options: options,
- logger: logger,
+ logger: logger,
}
+ // embedded.NewBus()
+ // embedded.NewScheduler()
+
s.bus = memory.NewBus()
- db, err := sqlite.Open(s.options.ConnectionString(), s.logger, s.bus)
+ db, err := sqlite.Open(options.ConnectionString(), s.logger, s.bus)
if err != nil {
return nil, err
@@ -81,13 +82,13 @@ func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) {
return nil, err
}
- s.scheduler = bus.NewScheduler(s.schedulerStore, s.logger, s.bus, s.options.RunnersPollInterval(),
+ s.scheduler = bus.NewScheduler(s.schedulerStore, s.logger, s.bus, options.RunnersPollInterval(),
bus.WorkerGroup{
- Size: s.options.RunnersDeploymentCount(),
+ Size: options.RunnersDeploymentCount(),
Messages: []string{deploy.Command{}.Name_()},
},
bus.WorkerGroup{
- Size: s.options.RunnersCleanupCount(),
+ Size: options.RunnersCleanupCount(),
Messages: []string{
cleanup_app.Command{}.Name_(),
delete_app.Command{}.Name_(),
@@ -105,7 +106,7 @@ func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) {
// Setups deployment infrastructure
if err = deploymentinfra.Setup(
- s.options,
+ options,
s.logger,
s.db,
s.bus,
@@ -125,7 +126,7 @@ func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) {
}
// Create the target needed to expose seelf itself and manage certificates if needed
- if exposedUrl, isSet := s.options.AppExposedUrl().TryGet(); isSet {
+ if exposedUrl, isSet := options.AppExposedUrl().TryGet(); isSet {
container := exposedUrl.User().Get("")
s.logger.Infow("exposing seelf container using the local target, creating it if needed, the container may restart once done",
diff --git a/cmd/version/version.go b/cmd/version/version.go
index c30f1894..387778dc 100644
--- a/cmd/version/version.go
+++ b/cmd/version/version.go
@@ -4,7 +4,7 @@ import (
"runtime/debug"
)
-var version = "2.3.2"
+var version = "2.4.0-next.1"
// Retrieve the currentVersion version with additional vcs info if any.
func Current() string {
diff --git a/docs/contributing/backend.md b/docs/contributing/backend.md
index 3582a021..877ab1f8 100644
--- a/docs/contributing/backend.md
+++ b/docs/contributing/backend.md
@@ -7,7 +7,7 @@ The **seelf** backend is written in the [Golang](https://go.dev/) language for i
### Packages overview
- `cmd/`: contains application commands such as the `serve` one
-- `internal/`: contains internal package representing the **core features** of this application organized by bounded contexts and `app`, `domain` and `infra` folders (see [The Domain](#the-domain))
+- `internal/`: contains internal package representing the **core features** of this application organized by bounded contexts and `app`, `domain`, `infra` and `fixture` folders (see [The Domain](#the-domain))
- `pkg/`: contains reusable stuff not tied to seelf which can be reused if needed
### The Domain {#the-domain}
@@ -19,6 +19,7 @@ The `internal/` follows a classic DDD structure with:
- `app`: commands and queries to orchestrate the domain logic
- `domain`: core stuff, entities and values objects, as pure as possible to be easily testable
- `infra`: implementation of domain specific interfaces for the current context
+- `fixture`: test helpers, mostly for generating correct and random aggregates satisfying needed state
In Go, it's common to see entities as structs with every field exposed. In this project, I have decided to try something else to prevent unwanted mutations from happening and making things more explicit.
diff --git a/docs/guide/migration.md b/docs/guide/migration.md
index b416d8ff..b0976782 100644
--- a/docs/guide/migration.md
+++ b/docs/guide/migration.md
@@ -5,7 +5,7 @@ Most of the time, you don't have to act manually when upgrading **seelf**. Howev
## From v1 to v2 {#v2}
::: warning
-This migration assumes you have **at least one application** on your seelf instance. If you do not have any application yet, you **should** remove everything and go back to [installing seelf](/guide/installation).
+This migration assumes you have **at least one application you care about** on your seelf instance. If you do not have any application yet, you **should** remove everything and go back to [installing seelf](/guide/installation) since it will be easier.
:::
The **seelf** `v2` introduces some breaking changes. When coming from the `v1.x.x`, you will need to take manual actions.
diff --git a/docs/guide/updating.md b/docs/guide/updating.md
index 280a96b5..d37b6960 100644
--- a/docs/guide/updating.md
+++ b/docs/guide/updating.md
@@ -6,6 +6,10 @@ This procedure depends on the method you choose when [installing seelf](/guide/i
When switching from a major version to another one (ex. `v1.x.x` to `v2.x.x`), check the [Migration page](/guide/migration) for additional instructions.
:::
+::: warning
+You should always **make a backup** before updating **seelf** to make sure you don't lose anything if something goes wrong.
+:::
+
## With Compose
Go where the initial `compose.yml` file has been created and run:
@@ -28,4 +32,4 @@ docker pull yuukanoo/seelf && docker rm $(docker stop $(docker ps -a -q --filter
## From sources
-Simply build the application again with the latest sources and you're good to go.
+Simply build the application again with the latest sources, run it and you're good to go.
diff --git a/docs/reference/providers/docker.md b/docs/reference/providers/docker.md
index 6a51513c..6f1795d4 100644
--- a/docs/reference/providers/docker.md
+++ b/docs/reference/providers/docker.md
@@ -35,7 +35,7 @@ Where `ENVIRONMENT` will be one of `production`, `staging`.
## Exposing services
-Once a valid compose file has been found, **seelf** will apply some **heuristics** to determine which services should be exposed and where.
+Once a valid compose file has been found and **only if** the target [manages the proxy itself](/reference/targets#proxy), **seelf** will apply some **heuristics** to determine which services should be exposed and where.
It will consider any service with **port mappings** to be exposed.
diff --git a/docs/reference/targets.md b/docs/reference/targets.md
index 5dedd74a..9d24c58c 100644
--- a/docs/reference/targets.md
+++ b/docs/reference/targets.md
@@ -6,9 +6,16 @@ Targets represents an **host** where your deployments will be exposed. When conf
For now, only one target per host is allowed.
:::
-## Url
+## Proxy configuration {#proxy}
-The url **determine where your applications will be made available**. It should be a **root url** as applications will use subdomains on it.
+When declaring a target, you must choose how the proxy (needed to make your services available from the outside world) should be managed:
+
+- **Automatic**: **seelf** will deploy and configure a [traefik](https://traefik.io/traefik/) proxy on the target. Services urls will be automatically generated based on the [target's url](#url) and [service file](/reference/providers/docker#exposing-services) when deploying. Exposed services will also join the proxy network.
+- **Manual**: you're in charge of **everything** related to services exposure. **seelf** will deploy services on this target without attempting to expose them in any way.
+
+### Url
+
+If the target manages the proxy itself, this url **determines where your applications will be made available**. It should be a **root url** as applications will use subdomains on it.
The scheme associated with this url (`http` or `https`) will determine if certificates should be generated or not.
diff --git a/go.mod b/go.mod
index c3c0ce8a..8bd03669 100644
--- a/go.mod
+++ b/go.mod
@@ -1,22 +1,22 @@
module github.com/YuukanOO/seelf
-go 1.21
+go 1.23
require (
- github.com/compose-spec/compose-go/v2 v2.0.3-0.20240407191136-f388192b8a39
- github.com/docker/cli v26.0.1+incompatible
- github.com/docker/compose/v2 v2.26.1
- github.com/docker/docker v26.0.1+incompatible
+ github.com/compose-spec/compose-go/v2 v2.1.6
+ github.com/docker/cli v27.1.2+incompatible
+ github.com/docker/compose/v2 v2.29.2
+ github.com/docker/docker v27.1.2+incompatible
github.com/docker/go-connections v0.5.0
- github.com/gin-gonic/gin v1.9.1
- github.com/go-git/go-git/v5 v5.11.0
- github.com/golang-migrate/migrate/v4 v4.16.2
+ github.com/gin-gonic/gin v1.10.0
+ github.com/go-git/go-git/v5 v5.12.0
+ github.com/golang-migrate/migrate/v4 v4.17.1
github.com/joho/godotenv v1.5.1
github.com/kevinburke/ssh_config v1.2.0
- github.com/mattn/go-sqlite3 v1.14.19
+ github.com/mattn/go-sqlite3 v1.14.22
github.com/segmentio/ksuid v1.0.4
- github.com/spf13/cobra v1.8.0
- go.uber.org/zap v1.26.0
+ github.com/spf13/cobra v1.8.1
+ go.uber.org/zap v1.27.0
)
require (
@@ -25,9 +25,8 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
- github.com/Microsoft/go-winio v0.6.1 // indirect
- github.com/Microsoft/hcsshim v0.11.4 // indirect
- github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
@@ -44,24 +43,29 @@ require (
github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/goterm v1.0.4 // indirect
- github.com/bytedance/sonic v1.10.2 // indirect
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
- github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
- github.com/chenzhuoyu/iasm v0.9.1 // indirect
- github.com/cloudflare/circl v1.3.6 // indirect
+ github.com/cloudflare/circl v1.3.7 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
github.com/containerd/console v1.0.4 // indirect
- github.com/containerd/containerd v1.7.13 // indirect
+ github.com/containerd/containerd v1.7.20 // indirect
+ github.com/containerd/containerd/api v1.7.19 // indirect
github.com/containerd/continuity v0.4.3 // indirect
+ github.com/containerd/errdefs v0.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
- github.com/containerd/ttrpc v1.2.2 // indirect
+ github.com/containerd/platforms v0.2.1 // indirect
+ github.com/containerd/ttrpc v1.2.5 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/distribution/reference v0.5.0 // indirect
- github.com/docker/buildx v0.13.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/buildx v0.16.2 // indirect
+ github.com/docker/cli-docs-tool v0.8.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
- github.com/docker/docker-credential-helpers v0.8.0 // indirect
+ github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
@@ -69,18 +73,19 @@ require (
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/fsnotify/fsevents v0.1.1 // indirect
+ github.com/fsnotify/fsevents v0.2.0 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
- github.com/go-logr/logr v1.3.0 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
- github.com/gofrs/flock v0.8.1 // indirect
+ github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -94,18 +99,17 @@ require (
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
- github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
- github.com/hashicorp/go-version v1.6.0 // indirect
+ github.com/hashicorp/go-version v1.7.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
- github.com/klauspost/compress v1.17.4 // indirect
- github.com/klauspost/cpuid/v2 v2.2.6 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
@@ -113,10 +117,9 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
- github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
- github.com/mitchellh/reflectwalk v1.0.2 // indirect
- github.com/moby/buildkit v0.13.1 // indirect
+ github.com/moby/buildkit v0.15.1 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
@@ -126,6 +129,7 @@ require (
github.com/moby/sys/signal v0.7.0 // indirect
github.com/moby/sys/symlink v0.2.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -133,30 +137,31 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
- github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
- github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
- github.com/sergi/go-diff v1.3.1 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
- github.com/skeema/knownhosts v1.2.1 // indirect
+ github.com/skeema/knownhosts v1.2.2 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/spf13/pflag v1.0.5 // indirect
- github.com/stretchr/testify v1.8.4 // indirect
+ github.com/stretchr/testify v1.9.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
- github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5 // indirect
+ github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c // indirect
+ github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
- github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 // indirect
+ github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
@@ -167,12 +172,11 @@ require (
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect
- go.opentelemetry.io/otel/exporters/prometheus v0.42.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
@@ -180,25 +184,21 @@ require (
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/arch v0.6.0 // indirect
- golang.org/x/mod v0.14.0 // indirect
- golang.org/x/net v0.20.0 // indirect
- golang.org/x/oauth2 v0.11.0 // indirect
- golang.org/x/sync v0.6.0 // indirect
- golang.org/x/term v0.16.0 // indirect
+ golang.org/x/arch v0.8.0 // indirect
+ golang.org/x/net v0.25.0 // indirect
+ golang.org/x/oauth2 v0.21.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
+ golang.org/x/term v0.20.0 // indirect
golang.org/x/time v0.3.0 // indirect
- golang.org/x/tools v0.17.0 // indirect
- google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
- google.golang.org/grpc v1.59.0 // indirect
+ google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
+ google.golang.org/grpc v1.60.1 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/api v0.29.2 // indirect
k8s.io/apimachinery v0.29.2 // indirect
- k8s.io/apiserver v0.29.2 // indirect
k8s.io/client-go v0.29.2 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
@@ -206,7 +206,7 @@ require (
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
- tags.cncf.io/container-device-interface v0.6.2 // indirect
+ tags.cncf.io/container-device-interface v0.7.2 // indirect
)
require (
@@ -215,28 +215,28 @@ require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/go-playground/validator/v10 v10.16.0 // indirect
- github.com/golang/protobuf v1.5.3 // indirect
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/leodido/go-urn v1.2.4 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/ugorji/go/codec v1.2.11 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.11.0 // indirect
- golang.org/x/crypto v0.18.0
+ golang.org/x/crypto v0.23.0
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3
- golang.org/x/sys v0.16.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- google.golang.org/protobuf v1.31.0 // indirect
+ golang.org/x/sys v0.22.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)
replace (
- github.com/docker/compose/v2 => github.com/YuukanOO/compose/v2 v2.0.0-20240416104623-b2a6b2de620d // Skip network aliases if external network (which is the case for targets proxy networks). It will prevent dns conflicts when resolving
+ github.com/docker/compose/v2 => github.com/YuukanOO/compose/v2 v2.27.2-0.20240821071151-110203bb5f25 // Skip network aliases if external network (which is the case for targets proxy networks). It will prevent dns conflicts when resolving
github.com/kevinburke/ssh_config => github.com/YuukanOO/ssh_config v0.0.0-20240416065040-22ccaddd8792 // Expose some additional stuff such as IsImplicit
)
diff --git a/go.sum b/go.sum
index 7ea9a49d..141106e5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,7 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME=
-cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0=
-cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
@@ -19,21 +17,21 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
-github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
-github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
+github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb h1:w9IDEB7P1VzNcBpOG7kMpFkZp2DkyJIUt0gDx5MBhRU=
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
-github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
-github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
+github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
-github.com/YuukanOO/compose/v2 v2.0.0-20240416104623-b2a6b2de620d h1:vtJ/MgHwydMofH9DqaRxvFtJHXFcFEfFNI3uU6bvLNk=
-github.com/YuukanOO/compose/v2 v2.0.0-20240416104623-b2a6b2de620d/go.mod h1:yIsLkV74jorkKFiA9szQUe9qA90r6raAK9uu10va0Qk=
+github.com/YuukanOO/compose/v2 v2.27.2-0.20240821071151-110203bb5f25 h1:sXyAIIJ42hbXuDgjo001SGp0ykcmwgY/1FaUtxZeLMc=
+github.com/YuukanOO/compose/v2 v2.27.2-0.20240821071151-110203bb5f25/go.mod h1:umytvxgECSU214WiMKqVtf7LXsAeLb0I/UFGYW5SPDU=
github.com/YuukanOO/ssh_config v0.0.0-20240416065040-22ccaddd8792 h1:INERq367kk1TRX9bsQkcKZdR801QVxS0vzDvTFLPuAE=
github.com/YuukanOO/ssh_config v0.0.0-20240416065040-22ccaddd8792/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
@@ -93,80 +91,82 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
-github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
-github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
-github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
-github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
-github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
-github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
-github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
-github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
-github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
-github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
+github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY=
+github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
-github.com/compose-spec/compose-go/v2 v2.0.3-0.20240407191136-f388192b8a39 h1:ZUpnv0xA75X9gy9Y7hjJm51nflGbr+2URaLXBtEic7A=
-github.com/compose-spec/compose-go/v2 v2.0.3-0.20240407191136-f388192b8a39/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc=
+github.com/compose-spec/compose-go/v2 v2.1.6 h1:d0Cs0DffmOwmSzs0YPHwKCskknGq2jfGg4uGowlEpps=
+github.com/compose-spec/compose-go/v2 v2.1.6/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
-github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is=
-github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4=
+github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ=
+github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0=
+github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA=
+github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig=
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
+github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
+github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.13.7 h1:x7DHvGnzJOu1ZPwPYkeOPk5MjZZYbdddygEjaSDoFTk=
github.com/containerd/nydus-snapshotter v0.13.7/go.mod h1:VPVKQ3jmHFIcUIV2yiQ1kImZuBFS3GXDohKs9mRABVE=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA=
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
-github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs=
-github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak=
+github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
+github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
-github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
-github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
+github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
-github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
-github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/buildx v0.13.1 h1:uZjBcb477zh02tnHk0rqNV/DZOxbf/OiHw6Mc8OhDYU=
-github.com/docker/buildx v0.13.1/go.mod h1:f2n6vggoX4sNNZ0XoRZ0Wtv6J1/rbDTabgdHtpW9NNM=
-github.com/docker/cli v26.0.1+incompatible h1:eZDuplk2jYqgUkNLDYwTBxqmY9cM3yHnmN6OIUEjL3U=
-github.com/docker/cli v26.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/buildx v0.16.2 h1:SPcyEiiCZEntJQ+V0lJI8ZudUrki2v1qUqmC/NqxDDs=
+github.com/docker/buildx v0.16.2/go.mod h1:by+CuE4Q+2NvECkIhNcWe89jjbHADCrDlzS9MRgbv2k=
+github.com/docker/cli v27.1.2+incompatible h1:nYviRv5Y+YAKx3dFrTvS1ErkyVVunKOhoweCTE1BsnI=
+github.com/docker/cli v27.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli-docs-tool v0.8.0 h1:YcDWl7rQJC3lJ7WVZRwSs3bc9nka97QLWfyJQli8yJU=
+github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsBIrW21a5pUbdk=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v26.0.1+incompatible h1:t39Hm6lpXuXtgkF0dm1t9a5HkbUfdGy6XbWexmGr+hA=
-github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
-github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
+github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY=
+github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
+github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
@@ -190,17 +190,13 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/fsnotify/fsevents v0.1.1 h1:/125uxJvvoSDDBPen6yUZbil8J9ydKZnnl3TWWmvnkw=
-github.com/fsnotify/fsevents v0.1.1/go.mod h1:+d+hS27T6k5J8CRaPLKFgwKYcpS7GwW3Ule9+SC2ZRc=
+github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
+github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
@@ -211,10 +207,10 @@ github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NY
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
-github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
-github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
-github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
-github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
+github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
@@ -222,15 +218,15 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
-github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
+github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
+github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
@@ -249,49 +245,47 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
-github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
-github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
+github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
-github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
+github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
-github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
+github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
+github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 h1:jc2UWq7CbdszqeH6qu1ougXMIUBfSy8Pbh/anURYbGI=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -299,8 +293,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
-github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -319,8 +313,6 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
-github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
@@ -332,8 +324,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
-github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
+github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -369,11 +361,11 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
-github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
-github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -387,11 +379,12 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
-github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -409,8 +402,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
-github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -420,15 +413,13 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
-github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
-github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
+github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
-github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/moby/buildkit v0.13.1 h1:L8afOFhPq2RPJJSr/VyzbufwID7jquZVB7oFHbPRcPE=
-github.com/moby/buildkit v0.13.1/go.mod h1:aNmNQKLBFYAOFuzQjR3VA27/FijlvtBD1pjNwTSN37k=
+github.com/moby/buildkit v0.15.1 h1:J6wrew7hphKqlq1wuu6yaUb/1Ra7gEzDAovylGztAKM=
+github.com/moby/buildkit v0.15.1/go.mod h1:Yis8ZMUJTHX9XhH9zVyK2igqSHV3sxi3UN0uztZocZk=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
@@ -447,6 +438,8 @@ github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZ
github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -479,16 +472,16 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
-github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg=
-github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
+github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
-github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -506,7 +499,6 @@ github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+L
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
@@ -518,9 +510,8 @@ github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o=
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8=
@@ -533,8 +524,8 @@ github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/
github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
-github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
-github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
@@ -542,13 +533,11 @@ github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
-github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
+github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
@@ -556,8 +545,8 @@ github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/u
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -570,6 +559,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -579,25 +569,27 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g=
-github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5 h1:oZS8KCqAg62sxJkEq/Ppzqrb6EooqzWtL8Oaex7bc5c=
-github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5/go.mod h1:vbbYqJlnswsbJqWUcJN8fKtBhnEgldDrcagTgnBVKKM=
+github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c h1:+6wg/4ORAbnSoGDzg2Q1i3CeMcT/jjhye/ZfnBHy7/M=
+github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c/go.mod h1:vbbYqJlnswsbJqWUcJN8fKtBhnEgldDrcagTgnBVKKM=
+github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
+github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
-github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs=
-github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
+github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
+github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
-github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -624,8 +616,8 @@ go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 h1:ZtfnDL+tUrs1F0Pzfwbg2d59Gru9NCH3bgSHBM6LDwU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0/go.mod h1:hG4Fj/y8TR/tlEDREo8tWstl9fO9gcFkn4xrx0Io8xU=
-go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 h1:NmnYCiR0qNufkldjVvyQfZTHSdzeHoZ41zggMsdMcLM=
-go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0/go.mod h1:UVAO61+umUsHLtYb8KXXRoHtxUkdOPkYidzW3gipRLQ=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 h1:wNMDy/LVGLj2h3p6zg4d0gypKfWKSWI14E1C4smOgl8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0/go.mod h1:YfbDdXAAkemWJK3H/DshvlrxqFB2rtW4rY6ky/3x/H0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
@@ -634,8 +626,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqhe
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I=
-go.opentelemetry.io/otel/exporters/prometheus v0.42.0 h1:jwV9iQdvp38fxXi8ZC+lNpxjK16MRcZlpDYvbuO1FiA=
-go.opentelemetry.io/otel/exporters/prometheus v0.42.0/go.mod h1:f3bYiqNqhoPxkvI2LrXqQVC546K7BuRDL/kKuxkujhA=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
@@ -646,22 +636,19 @@ go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
-golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -673,28 +660,19 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -706,30 +684,26 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
-golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -752,16 +726,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -770,15 +744,11 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -790,34 +760,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA=
-google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI=
-google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k=
-google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
+google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
+google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
+google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
+google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
-google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
+google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
@@ -849,14 +804,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A=
k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0=
k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8=
k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
-k8s.io/apiserver v0.29.2 h1:+Z9S0dSNr+CjnVXQePG8TcBWHr3Q7BmAr7NraHvsMiQ=
-k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ=
k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg=
k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
@@ -873,5 +824,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+s
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
-tags.cncf.io/container-device-interface v0.6.2 h1:dThE6dtp/93ZDGhqaED2Pu374SOeUkBfuvkLuiTdwzg=
-tags.cncf.io/container-device-interface v0.6.2/go.mod h1:Shusyhjs1A5Na/kqPVLL0KqnHQHuunol9LFeUNkuGVE=
+tags.cncf.io/container-device-interface v0.7.2 h1:MLqGnWfOr1wB7m08ieI4YJ3IoLKKozEnnNYBtacDPQU=
+tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto=
diff --git a/internal/auth/app/create_first_account/create_first_account_test.go b/internal/auth/app/create_first_account/create_first_account_test.go
index 2cc8392a..a2d8fb25 100644
--- a/internal/auth/app/create_first_account/create_first_account_test.go
+++ b/internal/auth/app/create_first_account/create_first_account_test.go
@@ -6,71 +6,83 @@ import (
"github.com/YuukanOO/seelf/internal/auth/app/create_first_account"
"github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/auth/infra/crypto"
- "github.com/YuukanOO/seelf/internal/auth/infra/memory"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/validate"
)
func Test_CreateFirstAccount(t *testing.T) {
- ctx := context.Background()
- hasher := crypto.NewBCryptHasher()
- keygen := crypto.NewKeyGenerator()
- sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, create_first_account.Command] {
- store := memory.NewUsersStore(existingUsers...)
- return create_first_account.Handler(store, store, hasher, keygen)
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, create_first_account.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return create_first_account.Handler(context.UsersStore, context.UsersStore, crypto.NewBCryptHasher(), crypto.NewKeyGenerator()), context.Dispatcher
}
t.Run("should returns the existing user id if a user already exists", func(t *testing.T) {
- usr := must.Panic(domain.NewUser(domain.NewEmailRequirement("existing@example.com", true), "password", "apikey"))
- uc := sut(&usr)
+ existingUser := fixture.User()
+ handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser))
- uid, err := uc(ctx, create_first_account.Command{})
+ uid, err := handler(context.Background(), create_first_account.Command{})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(usr.ID()), uid)
+ assert.Nil(t, err)
+ assert.Equal(t, string(existingUser.ID()), uid)
+ assert.HasLength(t, 0, dispatcher.Signals())
})
t.Run("should require both email and password or fail with ErrAdminAccountRequired", func(t *testing.T) {
- uc := sut()
- uid, err := uc(ctx, create_first_account.Command{})
+ handler, _ := arrange(t)
+ uid, err := handler(context.Background(), create_first_account.Command{})
- testutil.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err)
- testutil.Equals(t, "", uid)
+ assert.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err)
+ assert.Equal(t, "", uid)
- uid, err = uc(ctx, create_first_account.Command{Email: "admin@example.com"})
- testutil.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err)
- testutil.Equals(t, "", uid)
-
- uid, err = uc(ctx, create_first_account.Command{Password: "admin"})
- testutil.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err)
- testutil.Equals(t, "", uid)
+ uid, err = handler(context.Background(), create_first_account.Command{Email: "admin@example.com"})
+ assert.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err)
+ assert.Equal(t, "", uid)
+ uid, err = handler(context.Background(), create_first_account.Command{Password: "admin"})
+ assert.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err)
+ assert.Equal(t, "", uid)
})
t.Run("should require valid inputs", func(t *testing.T) {
- uc := sut()
- uid, err := uc(ctx, create_first_account.Command{
- Email: "notanemail",
+ handler, _ := arrange(t)
+ uid, err := handler(context.Background(), create_first_account.Command{
+ Email: "not_an_email",
Password: "admin",
})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- testutil.Equals(t, "", uid)
-
+ assert.Equal(t, "", uid)
+ assert.ValidationError(t, validate.FieldErrors{
+ "email": domain.ErrInvalidEmail,
+ }, err)
})
t.Run("should creates the first user account if everything is good", func(t *testing.T) {
- uc := sut()
- uid, err := uc(ctx, create_first_account.Command{
+ handler, dispatcher := arrange(t)
+ uid, err := handler(context.Background(), create_first_account.Command{
Email: "admin@example.com",
Password: "admin",
})
- testutil.IsNil(t, err)
- testutil.NotEquals(t, "", uid)
+ assert.Nil(t, err)
+ assert.NotEqual(t, "", uid)
+
+ assert.HasLength(t, 1, dispatcher.Signals())
+ registered := assert.Is[domain.UserRegistered](t, dispatcher.Signals()[0])
+
+ assert.Equal(t, domain.UserRegistered{
+ ID: domain.UserID(uid),
+ Email: "admin@example.com",
+ Password: assert.NotZero(t, registered.Password),
+ RegisteredAt: assert.NotZero(t, registered.RegisteredAt),
+ Key: assert.NotZero(t, registered.Key),
+ }, registered)
})
}
diff --git a/internal/auth/app/login/login_test.go b/internal/auth/app/login/login_test.go
index a304d4c5..70a011de 100644
--- a/internal/auth/app/login/login_test.go
+++ b/internal/auth/app/login/login_test.go
@@ -6,66 +6,81 @@ import (
"github.com/YuukanOO/seelf/internal/auth/app/login"
"github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/auth/infra/crypto"
- "github.com/YuukanOO/seelf/internal/auth/infra/memory"
- "github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/validate"
+ "github.com/YuukanOO/seelf/pkg/validate/strings"
)
func Test_Login(t *testing.T) {
hasher := crypto.NewBCryptHasher()
- password := must.Panic(hasher.Hash("password")) // Sample password hash for the string "password" for tests
- existingUser := must.Panic(domain.NewUser(domain.NewEmailRequirement("existing@example.com", true), password, "apikey"))
- sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, login.Command] {
- store := memory.NewUsersStore(existingUsers...)
- return login.Handler(store, hasher)
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, login.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return login.Handler(context.UsersStore, hasher), context.Dispatcher
}
t.Run("should require valid inputs", func(t *testing.T) {
- uc := sut()
- _, err := uc(context.Background(), login.Command{})
+ handler, _ := arrange(t)
+ _, err := handler(context.Background(), login.Command{})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
+ assert.ValidationError(t, validate.FieldErrors{
+ "email": domain.ErrInvalidEmail,
+ "password": strings.ErrRequired,
+ }, err)
})
t.Run("should complains if email does not exists", func(t *testing.T) {
- uc := sut()
- _, err := uc(context.Background(), login.Command{
+ handler, _ := arrange(t)
+ _, err := handler(context.Background(), login.Command{
Email: "notexisting@example.com",
- Password: "nobodycares",
+ Password: "no_body_cares",
})
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["email"])
- testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["password"])
+ assert.ValidationError(t, validate.FieldErrors{
+ "email": domain.ErrInvalidEmailOrPassword,
+ "password": domain.ErrInvalidEmailOrPassword,
+ }, err)
})
t.Run("should complains if password does not match", func(t *testing.T) {
- uc := sut(&existingUser)
- _, err := uc(context.Background(), login.Command{
+ existingUser := fixture.User(
+ fixture.WithEmail("existing@example.com"),
+ fixture.WithPassword("raw_password_hash", hasher),
+ )
+ handler, _ := arrange(t, fixture.WithUsers(&existingUser))
+
+ _, err := handler(context.Background(), login.Command{
Email: "existing@example.com",
- Password: "nobodycares",
+ Password: "no_body_cares",
})
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["email"])
- testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["password"])
+ assert.ValidationError(t, validate.FieldErrors{
+ "email": domain.ErrInvalidEmailOrPassword,
+ "password": domain.ErrInvalidEmailOrPassword,
+ }, err)
})
t.Run("should returns a valid user id if it succeeds", func(t *testing.T) {
- uc := sut(&existingUser)
- uid, err := uc(context.Background(), login.Command{
+ existingUser := fixture.User(
+ fixture.WithEmail("existing@example.com"),
+ fixture.WithPassword("password", hasher),
+ )
+ handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser))
+
+ uid, err := handler(context.Background(), login.Command{
Email: "existing@example.com",
Password: "password",
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(existingUser.ID()), uid)
+ assert.Nil(t, err)
+ assert.Equal(t, string(existingUser.ID()), uid)
+ assert.HasLength(t, 0, dispatcher.Signals())
})
}
diff --git a/internal/auth/app/refresh_api_key/refresh_api_key_test.go b/internal/auth/app/refresh_api_key/refresh_api_key_test.go
index bfe84bdc..1f6d8c63 100644
--- a/internal/auth/app/refresh_api_key/refresh_api_key_test.go
+++ b/internal/auth/app/refresh_api_key/refresh_api_key_test.go
@@ -6,43 +6,49 @@ import (
"github.com/YuukanOO/seelf/internal/auth/app/refresh_api_key"
"github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/auth/infra/crypto"
- "github.com/YuukanOO/seelf/internal/auth/infra/memory"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
)
func Test_RefreshApiKey(t *testing.T) {
- sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, refresh_api_key.Command] {
- store := memory.NewUsersStore(existingUsers...)
- return refresh_api_key.Handler(store, store, crypto.NewKeyGenerator())
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, refresh_api_key.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return refresh_api_key.Handler(context.UsersStore, context.UsersStore, crypto.NewKeyGenerator()), context.Dispatcher
}
t.Run("should fail if the user does not exists", func(t *testing.T) {
- uc := sut()
+ handler, _ := arrange(t)
- _, err := uc(context.Background(), refresh_api_key.Command{})
+ _, err := handler(context.Background(), refresh_api_key.Command{})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
})
t.Run("should refresh the user's API key if everything is good", func(t *testing.T) {
- user := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey"))
- uc := sut(&user)
+ existingUser := fixture.User()
+ handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser))
- key, err := uc(context.Background(), refresh_api_key.Command{
- ID: string(user.ID())},
+ key, err := handler(context.Background(), refresh_api_key.Command{
+ ID: string(existingUser.ID())},
)
- testutil.IsNil(t, err)
- testutil.NotEquals(t, "", key)
+ assert.Nil(t, err)
+ assert.NotEqual(t, "", key)
- evt := testutil.EventIs[domain.UserAPIKeyChanged](t, &user, 1)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ keyChanged := assert.Is[domain.UserAPIKeyChanged](t, dispatcher.Signals()[0])
- testutil.Equals(t, user.ID(), evt.ID)
- testutil.Equals(t, key, string(evt.Key))
+ assert.Equal(t, domain.UserAPIKeyChanged{
+ ID: existingUser.ID(),
+ Key: domain.APIKey(key),
+ }, keyChanged)
})
}
diff --git a/internal/auth/app/update_user/update_user_test.go b/internal/auth/app/update_user/update_user_test.go
index 386455cf..4124bb6b 100644
--- a/internal/auth/app/update_user/update_user_test.go
+++ b/internal/auth/app/update_user/update_user_test.go
@@ -6,79 +6,110 @@ import (
"github.com/YuukanOO/seelf/internal/auth/app/update_user"
"github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/auth/infra/crypto"
- "github.com/YuukanOO/seelf/internal/auth/infra/memory"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/monad"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/YuukanOO/seelf/pkg/validate"
)
func Test_UpdateUser(t *testing.T) {
- hasher := crypto.NewBCryptHasher()
- passwordHash := must.Panic(hasher.Hash("apassword"))
- sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, update_user.Command] {
- store := memory.NewUsersStore(existingUsers...)
- return update_user.Handler(store, store, hasher)
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, update_user.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return update_user.Handler(context.UsersStore, context.UsersStore, crypto.NewBCryptHasher()), context.Dispatcher
}
+ t.Run("should require an existing user", func(t *testing.T) {
+ handler, _ := arrange(t)
+ _, err := handler(context.Background(), update_user.Command{})
+
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ })
+
t.Run("should require valid inputs", func(t *testing.T) {
- uc := sut()
- _, err := uc(context.Background(), update_user.Command{})
+ handler, _ := arrange(t)
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ _, err := handler(context.Background(), update_user.Command{
+ Email: monad.Value("notanemail"),
+ })
+
+ assert.ValidationError(t, validate.FieldErrors{
+ "email": domain.ErrInvalidEmail,
+ }, err)
})
t.Run("should fail if the email is taken by another user", func(t *testing.T) {
- john := must.Panic(domain.NewUser(domain.NewEmailRequirement("john@doe.com", true), passwordHash, "anapikey"))
- jane := must.Panic(domain.NewUser(domain.NewEmailRequirement("jane@doe.com", true), passwordHash, "anapikey"))
+ john := fixture.User(fixture.WithEmail("john@doe.com"))
+ jane := fixture.User(fixture.WithEmail("jane@doe.com"))
- uc := sut(&john, &jane)
+ handler, _ := arrange(t, fixture.WithUsers(&john, &jane))
- _, err := uc(context.Background(), update_user.Command{
+ _, err := handler(context.Background(), update_user.Command{
ID: string(john.ID()),
Email: monad.Value("jane@doe.com"),
})
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrEmailAlreadyTaken, validationErr["email"])
+ assert.ValidationError(t, validate.FieldErrors{
+ "email": domain.ErrEmailAlreadyTaken,
+ }, err)
})
t.Run("should succeed if values are the same", func(t *testing.T) {
- john := must.Panic(domain.NewUser(domain.NewEmailRequirement("john@doe.com", true), passwordHash, "anapikey"))
- uc := sut(&john)
+ existingUser := fixture.User(fixture.WithEmail("john@doe.com"))
+ handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser))
- id, err := uc(context.Background(), update_user.Command{
- ID: string(john.ID()),
+ id, err := handler(context.Background(), update_user.Command{
+ ID: string(existingUser.ID()),
Email: monad.Value("john@doe.com"),
Password: monad.Value("apassword"),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(john.ID()), id)
- testutil.HasNEvents(t, &john, 2) // 2 since bcrypt will produce different hashes
- testutil.EventIs[domain.UserPasswordChanged](t, &john, 1)
+ assert.Nil(t, err)
+ assert.Equal(t, string(existingUser.ID()), id)
+
+ assert.HasLength(t, 1, dispatcher.Signals())
+ changed := assert.Is[domain.UserPasswordChanged](t, dispatcher.Signals()[0])
+
+ assert.Equal(t, domain.UserPasswordChanged{
+ ID: existingUser.ID(),
+ Password: changed.Password,
+ }, changed)
})
t.Run("should update user if everything is good", func(t *testing.T) {
- john := must.Panic(domain.NewUser(domain.NewEmailRequirement("john@doe.com", true), passwordHash, "anapikey"))
- uc := sut(&john)
+ existingUser := fixture.User()
+ handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser))
- id, err := uc(context.Background(), update_user.Command{
- ID: string(john.ID()),
+ id, err := handler(context.Background(), update_user.Command{
+ ID: string(existingUser.ID()),
Email: monad.Value("another@email.com"),
Password: monad.Value("anotherpassword"),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(john.ID()), id)
- testutil.HasNEvents(t, &john, 3)
- evt := testutil.EventIs[domain.UserEmailChanged](t, &john, 1)
- testutil.Equals(t, "another@email.com", string(evt.Email))
- testutil.EventIs[domain.UserPasswordChanged](t, &john, 2)
+ assert.Nil(t, err)
+ assert.Equal(t, string(existingUser.ID()), id)
+
+ assert.HasLength(t, 2, dispatcher.Signals())
+
+ passwordChanged := assert.Is[domain.UserPasswordChanged](t, dispatcher.Signals()[1])
+
+ assert.Equal(t, domain.UserPasswordChanged{
+ ID: existingUser.ID(),
+ Password: passwordChanged.Password,
+ }, passwordChanged)
+
+ emailChanged := assert.Is[domain.UserEmailChanged](t, dispatcher.Signals()[0])
+
+ assert.Equal(t, domain.UserEmailChanged{
+ ID: existingUser.ID(),
+ Email: "another@email.com",
+ }, emailChanged)
})
}
diff --git a/internal/auth/domain/context_test.go b/internal/auth/domain/context_test.go
index 0cecb5ad..b45566f7 100644
--- a/internal/auth/domain/context_test.go
+++ b/internal/auth/domain/context_test.go
@@ -5,22 +5,23 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/auth/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
+ "github.com/YuukanOO/seelf/pkg/monad"
)
-func Test_Auth_Context(t *testing.T) {
+func Test_AuthContext(t *testing.T) {
t.Run("should embed a user id into the context", func(t *testing.T) {
ctx := context.Background()
- uid := domain.UserID("auserid")
+ uid := domain.UserID("a_user_id")
newCtx := domain.WithUserID(ctx, uid)
- testutil.Equals(t, uid, domain.CurrentUser(newCtx).MustGet())
+ assert.Equal(t, uid, domain.CurrentUser(newCtx).MustGet())
})
t.Run("should returns an empty monad.Maybe if no user id has been attached to the context", func(t *testing.T) {
uid := domain.CurrentUser(context.Background())
- testutil.IsFalse(t, uid.HasValue())
+ assert.Equal(t, monad.None[domain.UserID](), uid)
})
}
diff --git a/internal/auth/domain/email_test.go b/internal/auth/domain/email_test.go
index 7b8ac0d9..1026a04a 100644
--- a/internal/auth/domain/email_test.go
+++ b/internal/auth/domain/email_test.go
@@ -4,15 +4,17 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/auth/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_Email_ValidatesAnEmail(t *testing.T) {
r, err := domain.EmailFrom("")
- testutil.Equals(t, "", r)
- testutil.ErrorIs(t, domain.ErrInvalidEmail, err)
+
+ assert.Equal(t, "", r)
+ assert.ErrorIs(t, domain.ErrInvalidEmail, err)
r, err = domain.EmailFrom("agood@email.com")
- testutil.Equals(t, "agood@email.com", r)
- testutil.IsNil(t, err)
+
+ assert.Equal(t, "agood@email.com", r)
+ assert.Nil(t, err)
}
diff --git a/internal/auth/domain/user_test.go b/internal/auth/domain/user_test.go
index 76441676..df56cf94 100644
--- a/internal/auth/domain/user_test.go
+++ b/internal/auth/domain/user_test.go
@@ -4,14 +4,14 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/auth/domain"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/internal/auth/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_User(t *testing.T) {
t.Run("should fail if the email is not available", func(t *testing.T) {
_, err := domain.NewUser(domain.NewEmailRequirement("an@email.com", false), "password", "apikey")
- testutil.Equals(t, domain.ErrEmailAlreadyTaken, err)
+ assert.ErrorIs(t, domain.ErrEmailAlreadyTaken, err)
})
t.Run("could be created", func(t *testing.T) {
@@ -23,58 +23,70 @@ func Test_User(t *testing.T) {
u, err := domain.NewUser(domain.NewEmailRequirement(email, true), password, key)
- testutil.IsNil(t, err)
- testutil.Equals(t, password, u.Password())
- testutil.NotEquals(t, "", u.ID())
+ assert.Nil(t, err)
+ assert.Equal(t, password, u.Password())
+ assert.NotZero(t, u.ID())
- registeredEvent := testutil.EventIs[domain.UserRegistered](t, &u, 0)
+ registeredEvent := assert.EventIs[domain.UserRegistered](t, &u, 0)
- testutil.Equals(t, u.ID(), registeredEvent.ID)
- testutil.Equals(t, email, registeredEvent.Email)
- testutil.Equals(t, u.Password(), registeredEvent.Password)
- testutil.Equals(t, key, registeredEvent.Key)
+ assert.Equal(t, domain.UserRegistered{
+ ID: u.ID(),
+ Email: email,
+ Password: password,
+ Key: key,
+ RegisteredAt: assert.NotZero(t, registeredEvent.RegisteredAt),
+ }, registeredEvent)
})
t.Run("should fail if trying to change for a non available email", func(t *testing.T) {
- u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey"))
+ existingUser := fixture.User()
- err := u.HasEmail(domain.NewEmailRequirement("one@email.com", false))
- testutil.Equals(t, domain.ErrEmailAlreadyTaken, err)
+ err := existingUser.HasEmail(domain.NewEmailRequirement("one@email.com", false))
+ assert.ErrorIs(t, domain.ErrEmailAlreadyTaken, err)
})
t.Run("should be able to change email", func(t *testing.T) {
- u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey"))
+ existingUser := fixture.User(fixture.WithEmail("some@email.com"))
- u.HasEmail(domain.NewEmailRequirement("some@email.com", true)) // no change, should not trigger events
- u.HasEmail(domain.NewEmailRequirement("newone@email.com", true))
+ assert.Nil(t, existingUser.HasEmail(domain.NewEmailRequirement("some@email.com", true)))
+ assert.Nil(t, existingUser.HasEmail(domain.NewEmailRequirement("newone@email.com", true)))
- testutil.HasNEvents(t, &u, 2)
- evt := testutil.EventIs[domain.UserEmailChanged](t, &u, 1)
- testutil.Equals(t, u.ID(), evt.ID)
- testutil.Equals(t, "newone@email.com", evt.Email)
+ assert.HasNEvents(t, 2, &existingUser, "should raise the event once per different email")
+ evt := assert.EventIs[domain.UserEmailChanged](t, &existingUser, 1)
+
+ assert.Equal(t, domain.UserEmailChanged{
+ ID: existingUser.ID(),
+ Email: "newone@email.com",
+ }, evt)
})
t.Run("should be able to change password", func(t *testing.T) {
- u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey"))
+ existingUser := fixture.User(fixture.WithPasswordHash("someHashedPassword"))
+
+ existingUser.HasPassword("someHashedPassword")
+ existingUser.HasPassword("anotherPassword")
- u.HasPassword("someHashedPassword") // no change, should not trigger events
- u.HasPassword("anotherPassword")
+ assert.HasNEvents(t, 2, &existingUser, "should raise the event once per different password")
+ evt := assert.EventIs[domain.UserPasswordChanged](t, &existingUser, 1)
- testutil.HasNEvents(t, &u, 2)
- evt := testutil.EventIs[domain.UserPasswordChanged](t, &u, 1)
- testutil.Equals(t, u.ID(), evt.ID)
- testutil.Equals(t, "anotherPassword", evt.Password)
+ assert.Equal(t, domain.UserPasswordChanged{
+ ID: existingUser.ID(),
+ Password: "anotherPassword",
+ }, evt)
})
t.Run("should be able to change API key", func(t *testing.T) {
- u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey"))
+ existingUser := fixture.User(fixture.WithAPIKey("apikey"))
+
+ existingUser.HasAPIKey("apikey")
+ existingUser.HasAPIKey("anotherKey")
- u.HasAPIKey("apikey") // no change, should not trigger events
- u.HasAPIKey("anotherKey")
+ assert.HasNEvents(t, 2, &existingUser, "should raise the event once per different API key")
+ evt := assert.EventIs[domain.UserAPIKeyChanged](t, &existingUser, 1)
- testutil.HasNEvents(t, &u, 2)
- evt := testutil.EventIs[domain.UserAPIKeyChanged](t, &u, 1)
- testutil.Equals(t, u.ID(), evt.ID)
- testutil.Equals(t, "anotherKey", evt.Key)
+ assert.Equal(t, domain.UserAPIKeyChanged{
+ ID: existingUser.ID(),
+ Key: "anotherKey",
+ }, evt)
})
}
diff --git a/internal/auth/fixture/database.go b/internal/auth/fixture/database.go
new file mode 100644
index 00000000..5b1cee60
--- /dev/null
+++ b/internal/auth/fixture/database.go
@@ -0,0 +1,88 @@
+//go:build !release
+
+package fixture
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/YuukanOO/seelf/cmd/config"
+ "github.com/YuukanOO/seelf/internal/auth/domain"
+ auth "github.com/YuukanOO/seelf/internal/auth/infra/sqlite"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ "github.com/YuukanOO/seelf/pkg/log"
+ "github.com/YuukanOO/seelf/pkg/must"
+ "github.com/YuukanOO/seelf/pkg/ostools"
+ "github.com/YuukanOO/seelf/pkg/storage/sqlite"
+)
+
+type (
+ seed struct {
+ users []*domain.User
+ }
+
+ Context struct {
+ Context context.Context // If users has been seeded, will be authenticated as the first one
+ Dispatcher spy.Dispatcher
+ UsersStore auth.UsersStore
+ }
+
+ SeedBuilder func(*seed)
+)
+
+func PrepareDatabase(t testing.TB, options ...SeedBuilder) *Context {
+ cfg := config.Default(config.WithTestDefaults())
+
+ if err := ostools.MkdirAll(cfg.DataDir()); err != nil {
+ t.Fatal(err)
+ }
+
+ result := Context{
+ Context: context.Background(),
+ Dispatcher: spy.NewDispatcher(),
+ }
+
+ db, err := sqlite.Open(cfg.ConnectionString(), must.Panic(log.NewLogger()), result.Dispatcher)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Cleanup(func() {
+ db.Close()
+ os.RemoveAll(cfg.DataDir())
+ })
+
+ if err = db.Migrate(auth.Migrations); err != nil {
+ t.Fatal(err)
+ }
+
+ result.UsersStore = auth.NewUsersStore(db)
+
+ // Seed the database
+ var s seed
+
+ for _, o := range options {
+ o(&s)
+ }
+
+ if len(s.users) > 0 {
+ if err := result.UsersStore.Write(result.Context, s.users...); err != nil {
+ t.Fatal(err)
+ }
+
+ result.Context = domain.WithUserID(result.Context, s.users[0].ID()) // The first created user will be used as the authenticated one
+ }
+
+ // Reset the dispatcher after seeding
+ result.Dispatcher.Reset()
+
+ return &result
+}
+
+func WithUsers(users ...*domain.User) SeedBuilder {
+ return func(s *seed) {
+ s.users = users
+ }
+}
diff --git a/internal/auth/fixture/database_test.go b/internal/auth/fixture/database_test.go
new file mode 100644
index 00000000..05f9472c
--- /dev/null
+++ b/internal/auth/fixture/database_test.go
@@ -0,0 +1,30 @@
+package fixture_test
+
+import (
+ "testing"
+
+ "github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/auth/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+)
+
+func Test_PrepareDatabase(t *testing.T) {
+ t.Run("should be able to prepare a database without seeding it", func(t *testing.T) {
+ ctx := fixture.PrepareDatabase(t)
+
+ assert.NotNil(t, ctx)
+ assert.NotNil(t, ctx.Dispatcher)
+ assert.NotNil(t, ctx.UsersStore)
+ assert.HasLength(t, 0, ctx.Dispatcher.Signals())
+ assert.HasLength(t, 0, ctx.Dispatcher.Requests())
+ })
+
+ t.Run("should seed users and attach the first user id to the created context", func(t *testing.T) {
+ user1 := fixture.User()
+ user2 := fixture.User()
+
+ ctx := fixture.PrepareDatabase(t, fixture.WithUsers(&user1, &user2))
+
+ assert.Equal(t, user1.ID(), domain.CurrentUser(ctx.Context).Get(""))
+ })
+}
diff --git a/internal/auth/fixture/user.go b/internal/auth/fixture/user.go
new file mode 100644
index 00000000..bd19d4f7
--- /dev/null
+++ b/internal/auth/fixture/user.go
@@ -0,0 +1,61 @@
+//go:build !release
+
+package fixture
+
+import (
+ "github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/pkg/id"
+ "github.com/YuukanOO/seelf/pkg/must"
+)
+
+type (
+ userOption struct {
+ email domain.Email
+ passwordHash domain.PasswordHash
+ apiKey domain.APIKey
+ }
+
+ UserOptionBuilder func(*userOption)
+)
+
+func User(options ...UserOptionBuilder) domain.User {
+ opts := userOption{
+ email: "john" + id.New[domain.Email]() + "@doe.com",
+ passwordHash: id.New[domain.PasswordHash](),
+ apiKey: id.New[domain.APIKey](),
+ }
+
+ for _, o := range options {
+ o(&opts)
+ }
+
+ return must.Panic(domain.NewUser(
+ domain.NewEmailRequirement(opts.email, true),
+ opts.passwordHash,
+ opts.apiKey,
+ ))
+}
+
+func WithEmail(email domain.Email) UserOptionBuilder {
+ return func(o *userOption) {
+ o.email = email
+ }
+}
+
+func WithPasswordHash(passwordHash domain.PasswordHash) UserOptionBuilder {
+ return func(o *userOption) {
+ o.passwordHash = passwordHash
+ }
+}
+
+func WithPassword(password string, hasher domain.PasswordHasher) UserOptionBuilder {
+ return func(o *userOption) {
+ o.passwordHash = must.Panic(hasher.Hash(password))
+ }
+}
+
+func WithAPIKey(apiKey domain.APIKey) UserOptionBuilder {
+ return func(o *userOption) {
+ o.apiKey = apiKey
+ }
+}
diff --git a/internal/auth/fixture/user_test.go b/internal/auth/fixture/user_test.go
new file mode 100644
index 00000000..ceac6e72
--- /dev/null
+++ b/internal/auth/fixture/user_test.go
@@ -0,0 +1,47 @@
+package fixture_test
+
+import (
+ "testing"
+
+ "github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/auth/fixture"
+ "github.com/YuukanOO/seelf/internal/auth/infra/crypto"
+ "github.com/YuukanOO/seelf/pkg/assert"
+)
+
+func Test_User(t *testing.T) {
+ t.Run("should be able to create a random user", func(t *testing.T) {
+ user := fixture.User()
+
+ assert.NotZero(t, user.ID())
+ })
+
+ t.Run("should be able to create a user with a given email", func(t *testing.T) {
+ user := fixture.User(fixture.WithEmail("an@email.com"))
+
+ registered := assert.EventIs[domain.UserRegistered](t, &user, 0)
+ assert.Equal(t, "an@email.com", registered.Email)
+ })
+
+ t.Run("should be able to create a user with a given password hash", func(t *testing.T) {
+ user := fixture.User(fixture.WithPasswordHash("somePassword"))
+
+ registered := assert.EventIs[domain.UserRegistered](t, &user, 0)
+ assert.Equal(t, "somePassword", registered.Password)
+ })
+
+ t.Run("should be able to create a user with a given password", func(t *testing.T) {
+ hasher := crypto.NewBCryptHasher()
+ user := fixture.User(fixture.WithPassword("somePassword", hasher))
+
+ registered := assert.EventIs[domain.UserRegistered](t, &user, 0)
+ assert.Nil(t, hasher.Compare("somePassword", registered.Password))
+ })
+
+ t.Run("should be able to create a user with a given api key", func(t *testing.T) {
+ user := fixture.User(fixture.WithAPIKey("someapikey"))
+
+ registered := assert.EventIs[domain.UserRegistered](t, &user, 0)
+ assert.Equal(t, "someapikey", registered.Key)
+ })
+}
diff --git a/internal/auth/infra/crypto/api_key_generator_test.go b/internal/auth/infra/crypto/api_key_generator_test.go
index 0aa181f0..e6188538 100644
--- a/internal/auth/infra/crypto/api_key_generator_test.go
+++ b/internal/auth/infra/crypto/api_key_generator_test.go
@@ -4,14 +4,15 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/auth/infra/crypto"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_KeyGenerator(t *testing.T) {
t.Run("should generate an API key", func(t *testing.T) {
generator := crypto.NewKeyGenerator()
key, err := generator.Generate()
- testutil.IsNil(t, err)
- testutil.HasNChars(t, 64, key)
+
+ assert.Nil(t, err)
+ assert.HasNRunes(t, 64, key)
})
}
diff --git a/internal/auth/infra/crypto/bcrypt_hasher_test.go b/internal/auth/infra/crypto/bcrypt_hasher_test.go
index 8cf4882c..e7cccead 100644
--- a/internal/auth/infra/crypto/bcrypt_hasher_test.go
+++ b/internal/auth/infra/crypto/bcrypt_hasher_test.go
@@ -4,25 +4,25 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/auth/infra/crypto"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
"golang.org/x/crypto/bcrypt"
)
-var hasher = crypto.NewBCryptHasher()
+func Test_BCryptHasher(t *testing.T) {
+ hasher := crypto.NewBCryptHasher()
-func Test_BCryptHasher_ShouldHashPassword(t *testing.T) {
t.Run("should hash password", func(t *testing.T) {
hash, err := hasher.Hash("mysecretpassword")
- testutil.IsNil(t, err)
- testutil.HasNChars(t, 60, hash)
+ assert.Nil(t, err)
+ assert.HasNRunes(t, 60, hash)
})
t.Run("should compare password", func(t *testing.T) {
hash, _ := hasher.Hash("mysecretpassword")
err := hasher.Compare("mysecretpassword", hash)
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
err = hasher.Compare("anothersecretpassword", hash)
- testutil.IsTrue(t, err == bcrypt.ErrMismatchedHashAndPassword)
+ assert.ErrorIs(t, bcrypt.ErrMismatchedHashAndPassword, err)
})
}
diff --git a/internal/auth/infra/memory/users.go b/internal/auth/infra/memory/users.go
deleted file mode 100644
index 760061f2..00000000
--- a/internal/auth/infra/memory/users.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package memory
-
-import (
- "context"
- "errors"
- "slices"
-
- "github.com/YuukanOO/seelf/internal/auth/domain"
- "github.com/YuukanOO/seelf/pkg/apperr"
- "github.com/YuukanOO/seelf/pkg/event"
-)
-
-type (
- UsersStore interface {
- domain.UsersReader
- domain.UsersWriter
- }
-
- usersStore struct {
- users []*userData
- }
-
- userData struct {
- id domain.UserID
- key domain.APIKey
- email domain.Email
- value *domain.User
- }
-)
-
-func NewUsersStore(existingUsers ...*domain.User) UsersStore {
- s := &usersStore{}
-
- s.Write(context.Background(), existingUsers...)
-
- return s
-}
-
-func (s *usersStore) GetAdminUser(ctx context.Context) (domain.User, error) {
- if len(s.users) == 0 {
- return domain.User{}, apperr.ErrNotFound
- }
-
- return *s.users[0].value, nil
-}
-
-func (s *usersStore) CheckEmailAvailability(ctx context.Context, email domain.Email, excluded ...domain.UserID) (domain.EmailRequirement, error) {
- u, err := s.GetByEmail(ctx, email)
-
- return domain.NewEmailRequirement(email, errors.Is(err, apperr.ErrNotFound) || slices.Contains(excluded, u.ID())), nil
-}
-
-func (s *usersStore) GetByID(ctx context.Context, id domain.UserID) (domain.User, error) {
- for _, u := range s.users {
- if u.id == id {
- return *u.value, nil
- }
- }
-
- return domain.User{}, apperr.ErrNotFound
-}
-
-func (s *usersStore) GetByEmail(ctx context.Context, email domain.Email) (domain.User, error) {
- for _, u := range s.users {
- if u.email == email {
- return *u.value, nil
- }
- }
-
- return domain.User{}, apperr.ErrNotFound
-}
-
-func (s *usersStore) GetIDFromAPIKey(ctx context.Context, key domain.APIKey) (domain.UserID, error) {
- for _, u := range s.users {
- if u.key == key {
- return u.id, nil
- }
- }
-
- return "", apperr.ErrNotFound
-}
-
-func (s *usersStore) Write(ctx context.Context, users ...*domain.User) error {
- for _, user := range users {
- for _, e := range event.Unwrap(user) {
- switch evt := e.(type) {
- case domain.UserRegistered:
- var exist bool
- for _, a := range s.users {
- if a.id == evt.ID {
- exist = true
- break
- }
- }
-
- if exist {
- continue
- }
-
- s.users = append(s.users, &userData{
- id: evt.ID,
- email: evt.Email,
- key: evt.Key,
- value: user,
- })
- case domain.UserAPIKeyChanged:
- for _, u := range s.users {
- if u.id == evt.ID {
- u.key = evt.Key
- *u.value = *user
- break
- }
- }
- default:
- for _, u := range s.users {
- if u.id == user.ID() {
- *u.value = *user
- break
- }
- }
- }
- }
- }
-
- return nil
-}
diff --git a/internal/deployment/app/cleanup_app/cleanup_app_test.go b/internal/deployment/app/cleanup_app/cleanup_app_test.go
index b93738c5..263a8b43 100644
--- a/internal/deployment/app/cleanup_app/cleanup_app_test.go
+++ b/internal/deployment/app/cleanup_app/cleanup_app_test.go
@@ -2,60 +2,95 @@ package cleanup_app_test
import (
"context"
+ "errors"
"testing"
"time"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/cleanup_app"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
- "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
-type initialData struct {
- deployments []*domain.Deployment
- targets []*domain.Target
-}
-
func Test_CleanupApp(t *testing.T) {
- ctx := context.Background()
- sut := func(data initialData) (bus.RequestHandler[bus.UnitType, cleanup_app.Command], *dummyProvider) {
- targetsStore := memory.NewTargetsStore(data.targets...)
- deploymentsStore := memory.NewDeploymentsStore(data.deployments...)
- provider := &dummyProvider{}
- return cleanup_app.Handler(targetsStore, deploymentsStore, provider), provider
+ arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, cleanup_app.Command],
+ context.Context,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return cleanup_app.Handler(context.TargetsStore, context.DeploymentsStore, provider), context.Context
}
t.Run("should fail silently if the target does not exist anymore", func(t *testing.T) {
- uc, provider := sut(initialData{})
+ var provider mockProvider
+ handler, ctx := arrange(t, &provider)
- r, err := uc(ctx, cleanup_app.Command{})
+ r, err := handler(ctx, cleanup_app.Command{})
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
- testutil.IsFalse(t, provider.called)
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+ assert.False(t, provider.called)
})
- t.Run("should fail if the target is configuring", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "uid"))
- deployment := must.Panic(app.NewDeployment(1, raw.Data(""), domain.Production, "uid"))
- deployment.HasStarted()
- deployment.HasEnded(domain.Services{}, nil)
-
- uc, provider := sut(initialData{
- targets: []*domain.Target{&target},
- deployments: []*domain.Deployment{&deployment},
+ t.Run("should fail if at least one deployment is running", func(t *testing.T) {
+ var provider mockProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithDeploymentRequestedBy(user.ID()))
+ assert.Nil(t, deployment.HasStarted())
+
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ _, err := handler(ctx, cleanup_app.Command{
+ TargetID: string(target.ID()),
+ AppID: string(app.ID()),
+ Environment: string(domain.Production),
+ From: deployment.Requested().At().Add(-1 * time.Hour),
+ To: deployment.Requested().At().Add(1 * time.Hour),
})
- _, err := uc(ctx, cleanup_app.Command{
+ assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
+ assert.False(t, provider.called)
+ })
+
+ t.Run("should fail if the target is configuring and at least one successful deployment has been made", func(t *testing.T) {
+ var provider mockProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithDeploymentRequestedBy(user.ID()))
+ assert.Nil(t, deployment.HasStarted())
+ assert.Nil(t, deployment.HasEnded(domain.Services{}, nil))
+
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ _, err := handler(ctx, cleanup_app.Command{
TargetID: string(target.ID()),
AppID: string(app.ID()),
Environment: string(domain.Production),
@@ -63,65 +98,107 @@ func Test_CleanupApp(t *testing.T) {
To: deployment.Requested().At().Add(1 * time.Hour),
})
- testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
- testutil.IsFalse(t, provider.called)
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ assert.False(t, provider.called)
})
t.Run("should succeed if the target is being deleted", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
+ var provider mockProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
- target.RequestCleanup(false, "uid")
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- uc, provider := sut(initialData{
- targets: []*domain.Target{&target},
- })
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
- _, err := uc(ctx, cleanup_app.Command{
+ _, err := handler(ctx, cleanup_app.Command{
TargetID: string(target.ID()),
})
- testutil.IsNil(t, err)
- testutil.IsFalse(t, provider.called)
+ assert.Nil(t, err)
+ assert.False(t, provider.called)
})
t.Run("should succeed if no successful deployments has been made", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
- target.Configured(target.CurrentVersion(), nil, nil)
+ var provider mockProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
- uc, provider := sut(initialData{
- targets: []*domain.Target{&target},
- })
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
- _, err := uc(ctx, cleanup_app.Command{
+ _, err := handler(ctx, cleanup_app.Command{
TargetID: string(target.ID()),
})
- testutil.IsNil(t, err)
- testutil.IsFalse(t, provider.called)
+ assert.Nil(t, err)
+ assert.False(t, provider.called)
})
- t.Run("should succeed if the target is ready and successful deployments have been made", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
- target.Configured(target.CurrentVersion(), nil, nil)
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "uid"))
- deployment := must.Panic(app.NewDeployment(1, raw.Data(""), domain.Production, "uid"))
- deployment.HasStarted()
- deployment.HasEnded(domain.Services{}, nil)
-
- uc, provider := sut(initialData{
- targets: []*domain.Target{&target},
- deployments: []*domain.Deployment{&deployment},
+ t.Run("should fail if the target is not ready and a successful deployment has been made", func(t *testing.T) {
+ var provider mockProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ app := fixture.App(fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithDeploymentRequestedBy(user.ID()))
+ assert.Nil(t, deployment.HasStarted())
+ assert.Nil(t, deployment.HasEnded(domain.Services{}, nil))
+
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ _, err := handler(ctx, cleanup_app.Command{
+ TargetID: string(target.ID()),
+ AppID: string(app.ID()),
+ Environment: string(domain.Production),
+ From: deployment.Requested().At().Add(-1 * time.Hour),
+ To: deployment.Requested().At().Add(1 * time.Hour),
})
- _, err := uc(ctx, cleanup_app.Command{
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
+ assert.False(t, provider.called)
+ })
+
+ t.Run("should succeed if the target is ready and successful deployments have been made", func(t *testing.T) {
+ var provider mockProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ target.Configured(target.CurrentVersion(), nil, nil)
+ app := fixture.App(fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithDeploymentRequestedBy(user.ID()))
+ assert.Nil(t, deployment.HasStarted())
+ assert.Nil(t, deployment.HasEnded(domain.Services{}, nil))
+
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ _, err := handler(ctx, cleanup_app.Command{
TargetID: string(target.ID()),
AppID: string(app.ID()),
Environment: string(domain.Production),
@@ -129,17 +206,17 @@ func Test_CleanupApp(t *testing.T) {
To: deployment.Requested().At().Add(1 * time.Hour),
})
- testutil.IsNil(t, err)
- testutil.IsTrue(t, provider.called)
+ assert.Nil(t, err)
+ assert.True(t, provider.called)
})
}
-type dummyProvider struct {
+type mockProvider struct {
domain.Provider
called bool
}
-func (d *dummyProvider) Cleanup(_ context.Context, _ domain.AppID, _ domain.Target, _ domain.Environment, s domain.CleanupStrategy) error {
+func (d *mockProvider) Cleanup(_ context.Context, _ domain.AppID, _ domain.Target, _ domain.Environment, s domain.CleanupStrategy) error {
d.called = s != domain.CleanupStrategySkip
return nil
}
diff --git a/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go b/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go
index 2dc5043c..2ba7aee5 100644
--- a/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go
+++ b/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go
@@ -13,15 +13,13 @@ func OnAppCleanupRequestedHandler(scheduler bus.Scheduler) bus.SignalHandler[dom
return func(ctx context.Context, evt domain.AppCleanupRequested) error {
now := time.Now().UTC()
- err := scheduler.Queue(ctx, Command{
+ if err := scheduler.Queue(ctx, Command{
AppID: string(evt.ID),
Environment: string(domain.Production),
TargetID: string(evt.ProductionConfig.Target()),
From: evt.ProductionConfig.Version(),
To: now,
- }, bus.WithPolicy(bus.JobPolicyCancellable))
-
- if err != nil {
+ }, bus.WithPolicy(bus.JobPolicyCancellable)); err != nil {
return err
}
diff --git a/internal/deployment/app/cleanup_target/cleanup_target_test.go b/internal/deployment/app/cleanup_target/cleanup_target_test.go
index 19e6396a..a0af545e 100644
--- a/internal/deployment/app/cleanup_target/cleanup_target_test.go
+++ b/internal/deployment/app/cleanup_target/cleanup_target_test.go
@@ -5,61 +5,133 @@ import (
"errors"
"testing"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/cleanup_target"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_CleanupTarget(t *testing.T) {
- sut := func(existingTargets ...*domain.Target) (bus.RequestHandler[bus.UnitType, cleanup_target.Command], *dummyProvider) {
- targetsStore := memory.NewTargetsStore(existingTargets...)
- deploymentsStore := memory.NewDeploymentsStore()
- provider := &dummyProvider{}
- return cleanup_target.Handler(targetsStore, deploymentsStore, provider), provider
+
+ arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, cleanup_target.Command],
+ context.Context,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return cleanup_target.Handler(context.TargetsStore, context.DeploymentsStore, provider), context.Context
}
t.Run("should silently fail if the target does not exist anymore", func(t *testing.T) {
- uc, provider := sut()
+ var provider dummyProvider
+ handler, ctx := arrange(t, &provider)
- _, err := uc(context.Background(), cleanup_target.Command{})
+ _, err := handler(ctx, cleanup_target.Command{})
- testutil.IsNil(t, err)
- testutil.IsFalse(t, provider.called)
+ assert.Nil(t, err)
+ assert.False(t, provider.called)
})
- t.Run("should skip the provider cleanup if the target is not reachable", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
- target.Configured(target.CurrentVersion(), nil, errors.New("some error"))
+ t.Run("should skip the cleanup if the target has never been configured correctly", func(t *testing.T) {
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ target.Configured(target.CurrentVersion(), nil, errors.New("configuration_failed"))
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(ctx, cleanup_target.Command{
+ ID: string(target.ID()),
+ })
- uc, provider := sut(&target)
+ assert.Nil(t, err)
+ assert.False(t, provider.called)
+ })
- _, err := uc(context.Background(), cleanup_target.Command{
+ t.Run("should fail if a deployment is running on this target", func(t *testing.T) {
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithDeploymentRequestedBy(user.ID()))
+ assert.Nil(t, deployment.HasStarted())
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ _, err := handler(ctx, cleanup_target.Command{
ID: string(target.ID()),
})
- testutil.IsNil(t, err)
- testutil.IsFalse(t, provider.called)
+ assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
+ assert.False(t, provider.called)
})
- t.Run("should succeed if the target can be safely deleted", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
+ t.Run("should fail if being configured", func(t *testing.T) {
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(ctx, cleanup_target.Command{
+ ID: string(target.ID()),
+ })
+
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ assert.False(t, provider.called)
+ })
+
+ t.Run("should fail if has been configured in the past but is now unreachable", func(t *testing.T) {
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.Reconfigure())
+ target.Configured(target.CurrentVersion(), nil, errors.New("configuration_failed"))
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(ctx, cleanup_target.Command{
+ ID: string(target.ID()),
+ })
- uc, provider := sut(&target)
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
+ assert.False(t, provider.called)
+ })
+
+ t.Run("should cleanup the target if it is correctly configured", func(t *testing.T) {
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ target.Configured(target.CurrentVersion(), nil, nil)
+ handler, ctx := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
- _, err := uc(context.Background(), cleanup_target.Command{
+ _, err := handler(ctx, cleanup_target.Command{
ID: string(target.ID()),
})
- testutil.IsNil(t, err)
- testutil.IsTrue(t, provider.called)
+ assert.Nil(t, err)
+ assert.True(t, provider.called)
})
}
diff --git a/internal/deployment/app/configure_target/configure_target_test.go b/internal/deployment/app/configure_target/configure_target_test.go
index b0bfafcd..c7b85db0 100644
--- a/internal/deployment/app/configure_target/configure_target_test.go
+++ b/internal/deployment/app/configure_target/configure_target_test.go
@@ -6,83 +6,97 @@ import (
"testing"
"time"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/configure_target"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
)
func Test_ConfigureTarget(t *testing.T) {
- sut := func(existingTargets ...*domain.Target) (bus.RequestHandler[bus.UnitType, configure_target.Command], *dummyProvider) {
- provider := &dummyProvider{}
- store := memory.NewTargetsStore(existingTargets...)
- return configure_target.Handler(store, store, provider), provider
+
+ arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, configure_target.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return configure_target.Handler(context.TargetsStore, context.TargetsStore, provider), context.Dispatcher
}
t.Run("should fail silently if the target is not found", func(t *testing.T) {
- uc, provider := sut()
+ var provider dummyProvider
+ handler, _ := arrange(t, &provider)
- _, err := uc(context.Background(), configure_target.Command{})
+ _, err := handler(context.Background(), configure_target.Command{})
- testutil.IsNil(t, err)
- testutil.IsFalse(t, provider.called)
+ assert.Nil(t, err)
+ assert.False(t, provider.called)
})
t.Run("should returns early if the version is outdated", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
- created := testutil.EventIs[domain.TargetCreated](t, &target, 0)
- uc, provider := sut(&target)
-
- _, err := uc(context.Background(), configure_target.Command{
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(context.Background(), configure_target.Command{
ID: string(target.ID()),
- Version: created.State.Version().Add(-1 * time.Second),
+ Version: target.CurrentVersion().Add(-1 * time.Second),
})
- testutil.IsNil(t, err)
- testutil.IsFalse(t, provider.called)
+ assert.Nil(t, err)
+ assert.HasLength(t, 0, dispatcher.Signals())
+ assert.False(t, provider.called)
})
t.Run("should correctly mark the target as failed if the provider fails", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
- created := testutil.EventIs[domain.TargetCreated](t, &target, 0)
- uc, provider := sut(&target)
providerErr := errors.New("some error")
- provider.err = providerErr
-
- _, err := uc(context.Background(), configure_target.Command{
+ provider := dummyProvider{err: providerErr}
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(context.Background(), configure_target.Command{
ID: string(target.ID()),
- Version: created.State.Version(),
+ Version: target.CurrentVersion(),
})
- testutil.IsNil(t, err)
- testutil.IsTrue(t, provider.called)
- evt := testutil.EventIs[domain.TargetStateChanged](t, &target, 1)
- testutil.Equals(t, domain.TargetStatusFailed, evt.State.Status())
- testutil.Equals(t, providerErr.Error(), evt.State.ErrCode().MustGet())
+ assert.Nil(t, err)
+ assert.True(t, provider.called)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ changed := assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetStatusFailed, changed.State.Status())
+ assert.Equal(t, providerErr.Error(), changed.State.ErrCode().MustGet())
})
t.Run("should correctly mark the target as configured if everything is good", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
- created := testutil.EventIs[domain.TargetCreated](t, &target, 0)
- uc, provider := sut(&target)
-
- _, err := uc(context.Background(), configure_target.Command{
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t, &provider,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(context.Background(), configure_target.Command{
ID: string(target.ID()),
- Version: created.State.Version(),
+ Version: target.CurrentVersion(),
})
- testutil.IsNil(t, err)
- testutil.IsTrue(t, provider.called)
- evt := testutil.EventIs[domain.TargetStateChanged](t, &target, 1)
- testutil.Equals(t, domain.TargetStatusReady, evt.State.Status())
+ assert.Nil(t, err)
+ assert.True(t, provider.called)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ changed := assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetStatusReady, changed.State.Status())
+ assert.Equal(t, target.CurrentVersion(), changed.State.LastReadyVersion().MustGet())
})
}
diff --git a/internal/deployment/app/configure_target/on_app_cleanup_requested.go b/internal/deployment/app/configure_target/on_app_cleanup_requested.go
index b31141c2..273cb2a7 100644
--- a/internal/deployment/app/configure_target/on_app_cleanup_requested.go
+++ b/internal/deployment/app/configure_target/on_app_cleanup_requested.go
@@ -7,40 +7,38 @@ import (
"github.com/YuukanOO/seelf/pkg/bus"
)
-// When an application cleanup has been requested, unexpose the application from all targets.
+// When an application cleanup has been requested, un-expose the application from all targets.
func OnAppCleanupRequestedHandler(
reader domain.TargetsReader,
writer domain.TargetsWriter,
) bus.SignalHandler[domain.AppCleanupRequested] {
return func(ctx context.Context, evt domain.AppCleanupRequested) error {
- if evt.ProductionConfig.Target() == evt.StagingConfig.Target() {
- target, err := reader.GetByID(ctx, evt.ProductionConfig.Target())
-
- if err != nil {
- return err
- }
-
- target.UnExposeEntrypoints(evt.ID)
-
- return writer.Write(ctx, &target)
+ if err := unExpose(ctx, reader, writer, evt.ProductionConfig.Target(), evt.ID); err != nil {
+ return err
}
- productionTarget, err := reader.GetByID(ctx, evt.ProductionConfig.Target())
-
- if err != nil {
- return err
+ if evt.ProductionConfig.Target() == evt.StagingConfig.Target() {
+ return nil
}
- productionTarget.UnExposeEntrypoints(evt.ID, domain.Production)
+ return unExpose(ctx, reader, writer, evt.StagingConfig.Target(), evt.ID)
+ }
+}
- stagingTarget, err := reader.GetByID(ctx, evt.StagingConfig.Target())
+func unExpose(
+ ctx context.Context,
+ reader domain.TargetsReader,
+ writer domain.TargetsWriter,
+ id domain.TargetID,
+ app domain.AppID,
+) error {
+ target, err := reader.GetByID(ctx, id)
- if err != nil {
- return err
- }
+ if err != nil {
+ return err
+ }
- stagingTarget.UnExposeEntrypoints(evt.ID, domain.Staging)
+ target.UnExposeEntrypoints(app)
- return writer.Write(ctx, &productionTarget, &stagingTarget)
- }
+ return writer.Write(ctx, &target)
}
diff --git a/internal/deployment/app/configure_target/on_deployment_state_changed.go b/internal/deployment/app/configure_target/on_deployment_state_changed.go
index 4de3ad16..012269c5 100644
--- a/internal/deployment/app/configure_target/on_deployment_state_changed.go
+++ b/internal/deployment/app/configure_target/on_deployment_state_changed.go
@@ -22,7 +22,7 @@ func OnDeploymentStateChangedHandler(
return err
}
- target.ExposeEntrypoints(evt.ID.AppID(), evt.Config.Environment(), evt.State.Services().Get(domain.Services{}))
+ target.ExposeEntrypoints(evt.ID.AppID(), evt.Config.Environment(), evt.State.Services().Get(nil))
return writer.Write(ctx, &target)
}
diff --git a/internal/deployment/app/create_app/create_app.go b/internal/deployment/app/create_app/create_app.go
index 322a7ba9..235a04b7 100644
--- a/internal/deployment/app/create_app/create_app.go
+++ b/internal/deployment/app/create_app/create_app.go
@@ -102,7 +102,7 @@ func Handler(
vcs.Authenticated(token)
}
- app.UseVersionControl(vcs)
+ _ = app.UseVersionControl(vcs)
}
if err := writer.Write(ctx, &app); err != nil {
diff --git a/internal/deployment/app/create_app/create_app_test.go b/internal/deployment/app/create_app/create_app_test.go
index 806dbed7..c63c4aee 100644
--- a/internal/deployment/app/create_app/create_app_test.go
+++ b/internal/deployment/app/create_app/create_app_test.go
@@ -4,39 +4,81 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/create_app"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
+ "github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/validate"
+ "github.com/YuukanOO/seelf/pkg/validate/strings"
)
func Test_CreateApp(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- sut := func(existingApps ...*domain.App) bus.RequestHandler[string, create_app.Command] {
- store := memory.NewAppsStore(existingApps...)
- return create_app.Handler(store, store)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, create_app.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return create_app.Handler(context.AppsStore, context.AppsStore), context.Context, context.Dispatcher
}
t.Run("should require valid inputs", func(t *testing.T) {
- uc := sut()
- id, err := uc(ctx, create_app.Command{})
+ handler, ctx, _ := arrange(t)
+
+ id, err := handler(ctx, create_app.Command{})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- testutil.Equals(t, "", id)
+ assert.Zero(t, id)
+ assert.ValidationError(t, validate.FieldErrors{
+ "name": domain.ErrInvalidAppName,
+ "production.target": strings.ErrRequired,
+ "staging.target": strings.ErrRequired,
+ }, err)
})
t.Run("should fail if the name is already taken", func(t *testing.T) {
- a := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true), "uid"))
- uc := sut(&a)
+ user := authfixture.User()
+ productionTarget := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ stagingTarget := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ existingApp := fixture.App(fixture.WithAppName("my-app"),
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(productionTarget.ID()),
+ domain.NewEnvironmentConfig(stagingTarget.ID()),
+ ))
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&productionTarget, &stagingTarget),
+ fixture.WithApps(&existingApp),
+ )
- id, err := uc(ctx, create_app.Command{
+ id, err := handler(ctx, create_app.Command{
+ Name: "my-app",
+ Production: create_app.EnvironmentConfig{
+ Target: string(productionTarget.ID()),
+ },
+ Staging: create_app.EnvironmentConfig{
+ Target: string(stagingTarget.ID()),
+ },
+ })
+
+ assert.Zero(t, id)
+ assert.ValidationError(t, validate.FieldErrors{
+ "production.target": domain.ErrAppNameAlreadyTaken,
+ "staging.target": domain.ErrAppNameAlreadyTaken,
+ }, err)
+ })
+
+ t.Run("should fail if provided targets does not exists", func(t *testing.T) {
+ handler, ctx, _ := arrange(t)
+
+ id, err := handler(ctx, create_app.Command{
Name: "my-app",
Production: create_app.EnvironmentConfig{
Target: "production-target",
@@ -46,26 +88,53 @@ func Test_CreateApp(t *testing.T) {
},
})
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.Equals(t, "", id)
- testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["production.target"])
- testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["staging.target"])
+ assert.Zero(t, id)
+ assert.ValidationError(t, validate.FieldErrors{
+ "production.target": apperr.ErrNotFound,
+ "staging.target": apperr.ErrNotFound,
+ }, err)
})
t.Run("should create a new app if everything is good", func(t *testing.T) {
- uc := sut()
- id, err := uc(ctx, create_app.Command{
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ id, err := handler(ctx, create_app.Command{
Name: "my-app",
Production: create_app.EnvironmentConfig{
- Target: "production-target",
+ Target: string(target.ID()),
},
Staging: create_app.EnvironmentConfig{
- Target: "staging-target",
+ Target: string(target.ID()),
},
+ VersionControl: monad.Value(create_app.VersionControl{
+ Url: "https://somewhere.git",
+ Token: monad.Value("some-token"),
+ }),
})
- testutil.IsNil(t, err)
- testutil.NotEquals(t, "", id)
+ assert.Nil(t, err)
+ assert.NotZero(t, id)
+ assert.HasLength(t, 2, dispatcher.Signals())
+
+ created := assert.Is[domain.AppCreated](t, dispatcher.Signals()[0])
+ assert.DeepEqual(t, domain.AppCreated{
+ ID: domain.AppID(id),
+ Name: "my-app",
+ Production: created.Production,
+ Staging: created.Staging,
+ Created: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Created.At())),
+ }, created)
+ assert.Equal(t, target.ID(), created.Production.Target())
+ assert.Equal(t, target.ID(), created.Staging.Target())
+
+ versionControlConfigured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[1])
+ assert.Equal(t, created.ID, versionControlConfigured.ID)
+ assert.Equal(t, "https://somewhere.git", versionControlConfigured.Config.Url().String())
+ assert.Equal(t, "some-token", versionControlConfigured.Config.Token().Get(""))
})
}
diff --git a/internal/deployment/app/create_registry/create_registry_test.go b/internal/deployment/app/create_registry/create_registry_test.go
index 47fca249..ce74d319 100644
--- a/internal/deployment/app/create_registry/create_registry_test.go
+++ b/internal/deployment/app/create_registry/create_registry_test.go
@@ -4,52 +4,70 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/create_registry"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
- "github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/YuukanOO/seelf/pkg/validate"
+ "github.com/YuukanOO/seelf/pkg/validate/strings"
)
func Test_CreateRegistry(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- sut := func(existing ...*domain.Registry) bus.RequestHandler[string, create_registry.Command] {
- store := memory.NewRegistriesStore(existing...)
- return create_registry.Handler(store, store)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, create_registry.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return create_registry.Handler(context.RegistriesStore, context.RegistriesStore), context.Context, context.Dispatcher
}
t.Run("should require valid inputs", func(t *testing.T) {
- uc := sut()
- id, err := uc(ctx, create_registry.Command{})
+ handler, ctx, _ := arrange(t)
+
+ id, err := handler(ctx, create_registry.Command{})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- testutil.Equals(t, "", id)
+ assert.Zero(t, id)
+ assert.ValidationError(t, validate.FieldErrors{
+ "name": strings.ErrRequired,
+ "url": domain.ErrInvalidUrl,
+ }, err)
})
t.Run("should fail if the url is already taken", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- uc := sut(&r)
+ user := authfixture.User()
+ registry := fixture.Registry(
+ fixture.WithRegistryCreatedBy(user.ID()),
+ fixture.WithUrl(must.Panic(domain.UrlFrom("http://example.com"))),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithRegistries(®istry),
+ )
- id, err := uc(ctx, create_registry.Command{
+ id, err := handler(ctx, create_registry.Command{
Name: "registry",
Url: "http://example.com",
})
- testutil.Equals(t, "", id)
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validationErr["url"])
+ assert.Zero(t, id)
+ assert.ValidationError(t, validate.FieldErrors{
+ "url": domain.ErrUrlAlreadyTaken,
+ }, err)
})
t.Run("should create a new registry if everything is good", func(t *testing.T) {
- uc := sut()
+ user := authfixture.User()
+ handler, ctx, dispatcher := arrange(t, fixture.WithUsers(&user))
- id, err := uc(ctx, create_registry.Command{
+ id, err := handler(ctx, create_registry.Command{
Name: "registry",
Url: "http://example.com",
Credentials: monad.Value(create_registry.Credentials{
@@ -58,7 +76,22 @@ func Test_CreateRegistry(t *testing.T) {
}),
})
- testutil.NotEquals(t, "", id)
- testutil.IsNil(t, err)
+ assert.NotZero(t, id)
+ assert.Nil(t, err)
+ assert.HasLength(t, 2, dispatcher.Signals())
+
+ created := assert.Is[domain.RegistryCreated](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.RegistryCreated{
+ ID: domain.RegistryID(id),
+ Name: "registry",
+ Url: must.Panic(domain.UrlFrom("http://example.com")),
+ Created: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Created.At())),
+ }, created)
+
+ credentialsSet := assert.Is[domain.RegistryCredentialsChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.RegistryCredentialsChanged{
+ ID: domain.RegistryID(id),
+ Credentials: domain.NewCredentials("user", "password"),
+ }, credentialsSet)
})
}
diff --git a/internal/deployment/app/create_target/create_target.go b/internal/deployment/app/create_target/create_target.go
index cbf67faf..7636348f 100644
--- a/internal/deployment/app/create_target/create_target.go
+++ b/internal/deployment/app/create_target/create_target.go
@@ -6,6 +6,7 @@ import (
auth "github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/internal/deployment/domain"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/validate"
"github.com/YuukanOO/seelf/pkg/validate/strings"
)
@@ -13,9 +14,9 @@ import (
type Command struct {
bus.Command[string]
- Name string `json:"name"`
- Url string `json:"url"`
- Provider any `json:"-"`
+ Name string `json:"name"`
+ Url monad.Maybe[string] `json:"url"`
+ Provider any `json:"-"`
}
func (Command) Name_() string { return "deployment.command.create_target" }
@@ -30,7 +31,9 @@ func Handler(
if err := validate.Struct(validate.Of{
"name": validate.Field(cmd.Name, strings.Required),
- "url": validate.Value(cmd.Url, &targetUrl, domain.UrlFrom),
+ "url": validate.Maybe(cmd.Url, func(url string) error {
+ return validate.Value(url, &targetUrl, domain.UrlFrom)
+ }),
}); err != nil {
return "", err
}
@@ -42,10 +45,14 @@ func Handler(
}
// Validate availability of both the target domain and the config
- urlRequirement, err := reader.CheckUrlAvailability(ctx, targetUrl)
+ var urlRequirement domain.TargetUrlRequirement
- if err != nil {
- return "", err
+ if cmd.Url.HasValue() {
+ urlRequirement, err = reader.CheckUrlAvailability(ctx, targetUrl)
+
+ if err != nil {
+ return "", err
+ }
}
configRequirement, err := reader.CheckConfigAvailability(ctx, config)
@@ -55,7 +62,7 @@ func Handler(
}
if err = validate.Struct(validate.Of{
- "url": urlRequirement.Error(),
+ "url": validate.If(cmd.Url.HasValue(), urlRequirement.Error),
config.Kind(): configRequirement.Error(),
}); err != nil {
return "", err
@@ -63,7 +70,6 @@ func Handler(
target, err := domain.NewTarget(
cmd.Name,
- urlRequirement,
configRequirement,
auth.CurrentUser(ctx).MustGet(),
)
@@ -72,6 +78,12 @@ func Handler(
return "", err
}
+ if cmd.Url.HasValue() {
+ if err = target.ExposeServicesAutomatically(urlRequirement); err != nil {
+ return "", err
+ }
+ }
+
if err = writer.Write(ctx, &target); err != nil {
return "", err
}
diff --git a/internal/deployment/app/create_target/create_target_test.go b/internal/deployment/app/create_target/create_target_test.go
index 735c7365..02ce76fe 100644
--- a/internal/deployment/app/create_target/create_target_test.go
+++ b/internal/deployment/app/create_target/create_target_test.go
@@ -4,119 +4,141 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/create_target"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
- "github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
+ "github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/YuukanOO/seelf/pkg/validate"
+ "github.com/YuukanOO/seelf/pkg/validate/strings"
)
func Test_CreateTarget(t *testing.T) {
- var (
- uid auth.UserID = "uid"
- ctx = auth.WithUserID(context.Background(), uid)
- config dummyConfig
- )
- sut := func(existingTargets ...*domain.Target) bus.RequestHandler[string, create_target.Command] {
- store := memory.NewTargetsStore(existingTargets...)
-
- return create_target.Handler(store, store, &dummyProvider{})
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, create_target.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return create_target.Handler(context.TargetsStore, context.TargetsStore, &dummyProvider{}), context.Context, context.Dispatcher
}
t.Run("should require valid inputs", func(t *testing.T) {
- uc := sut()
+ handler, ctx, _ := arrange(t)
- _, err := uc(ctx, create_target.Command{})
+ _, err := handler(ctx, create_target.Command{})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
+ assert.ValidationError(t, validate.FieldErrors{
+ "name": strings.ErrRequired,
+ }, err)
})
- t.Run("should require a unique url", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true),
- domain.NewProviderConfigRequirement(config, true), uid))
+ t.Run("should require a unique url and config", func(t *testing.T) {
+ var config = fixture.ProviderConfig()
+ user := authfixture.User()
+ target := fixture.Target(
+ fixture.WithTargetCreatedBy(user.ID()),
+ fixture.WithProviderConfig(config),
+ )
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
- uc := sut(&target)
+ handler, ctx, _ := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target))
- _, err := uc(ctx, create_target.Command{
+ _, err := handler(ctx, create_target.Command{
Name: "target",
- Url: "http://example.com",
+ Url: monad.Value("http://example.com"),
Provider: config,
})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- validateError, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validateError["url"])
- testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, validateError[config.Kind()])
+ assert.ValidationError(t, validate.FieldErrors{
+ "url": domain.ErrUrlAlreadyTaken,
+ config.Kind(): domain.ErrConfigAlreadyTaken,
+ }, err)
})
t.Run("should require a valid provider config", func(t *testing.T) {
- uc := sut()
+ handler, ctx, _ := arrange(t)
- _, err := uc(ctx, create_target.Command{
+ _, err := handler(ctx, create_target.Command{
Name: "target",
- Url: "http://example.com",
})
- testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err)
+ assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
- t.Run("should require a unique provider config", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true),
- domain.NewProviderConfigRequirement(config, true), uid))
+ t.Run("should allow multiple manual targets to co-exists", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, ctx, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target))
- uc := sut(&target)
+ id, err := handler(ctx, create_target.Command{
+ Name: "target-one",
+ Provider: fixture.ProviderConfig(),
+ })
- _, err := uc(ctx, create_target.Command{
- Name: "target",
- Url: "http://another.example.com",
- Provider: config,
+ assert.Nil(t, err)
+ assert.NotZero(t, id)
+
+ id, err = handler(ctx, create_target.Command{
+ Name: "target-two",
+ Provider: fixture.ProviderConfig(),
})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- validateError, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, validateError[config.Kind()])
+ assert.Nil(t, err)
+ assert.NotZero(t, id)
+
+ assert.HasLength(t, 2, dispatcher.Signals())
})
t.Run("should create a new target", func(t *testing.T) {
- uc := sut()
+ var config = fixture.ProviderConfig()
+ user := authfixture.User()
+ handler, ctx, dispatcher := arrange(t, fixture.WithUsers(&user))
- id, err := uc(ctx, create_target.Command{
+ id, err := handler(ctx, create_target.Command{
Name: "target",
- Url: "http://example.com",
+ Url: monad.Value("http://example.com"),
Provider: config,
})
- testutil.IsNil(t, err)
- testutil.NotEquals(t, "", id)
+ assert.Nil(t, err)
+ assert.NotZero(t, id)
+ assert.HasLength(t, 3, dispatcher.Signals())
+
+ created := assert.Is[domain.TargetCreated](t, dispatcher.Signals()[0])
+ assert.DeepEqual(t, domain.TargetCreated{
+ ID: domain.TargetID(id),
+ Name: "target",
+ State: created.State,
+ Entrypoints: make(domain.TargetEntrypoints),
+ Provider: config, // Since the mock returns the config "as is"
+ Created: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Created.At())),
+ }, created)
+
+ urlChanged := assert.Is[domain.TargetUrlChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.TargetUrlChanged{
+ ID: domain.TargetID(id),
+ Url: must.Panic(domain.UrlFrom("http://example.com")),
+ }, urlChanged)
+
+ assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[2])
})
}
-type (
- dummyProvider struct {
- domain.Provider
- }
-
- dummyConfig struct{}
-)
+type dummyProvider struct {
+ domain.Provider
+}
func (*dummyProvider) Prepare(ctx context.Context, payload any, existing ...domain.ProviderConfig) (domain.ProviderConfig, error) {
if payload == nil {
return nil, domain.ErrNoValidProviderFound
}
- return dummyConfig{}, nil
+ return payload.(domain.ProviderConfig), nil
}
-
-func (dummyConfig) Fingerprint() string { return "dummy" }
-func (c dummyConfig) Equals(o domain.ProviderConfig) bool { return c == o }
-func (dummyConfig) Kind() string { return "dummy" }
-func (dummyConfig) String() string { return "dummy" }
diff --git a/internal/deployment/app/delete_app/delete_app_test.go b/internal/deployment/app/delete_app/delete_app_test.go
index 4967c35b..e5adab8b 100644
--- a/internal/deployment/app/delete_app/delete_app_test.go
+++ b/internal/deployment/app/delete_app/delete_app_test.go
@@ -2,76 +2,95 @@ package delete_app_test
import (
"context"
- "os"
"testing"
- "github.com/YuukanOO/seelf/cmd/config"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/delete_app"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/internal/deployment/infra/artifact"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/log"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
-func DeleteApp(t *testing.T) {
- ctx := context.Background()
- logger, _ := log.NewLogger()
+func Test_DeleteApp(t *testing.T) {
- sut := func(initialApps ...*domain.App) bus.RequestHandler[bus.UnitType, delete_app.Command] {
- opts := config.Default(config.WithTestDefaults())
- appsStore := memory.NewAppsStore(initialApps...)
- artifactManager := artifact.NewLocal(opts, logger)
-
- t.Cleanup(func() {
- os.RemoveAll(opts.DataDir())
- })
-
- return delete_app.Handler(appsStore, appsStore, artifactManager)
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, delete_app.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ logger, _ := log.NewLogger()
+ artifactManager := artifact.NewLocal(context.Config, logger)
+ return delete_app.Handler(context.AppsStore, context.AppsStore, artifactManager), context.Dispatcher
}
t.Run("should fail silently if the application does not exist anymore", func(t *testing.T) {
- uc := sut()
+ handler, dispatcher := arrange(t)
- r, err := uc(ctx, delete_app.Command{
+ r, err := handler(context.Background(), delete_app.Command{
ID: "some-id",
})
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+ assert.HasLength(t, 0, dispatcher.Signals())
})
- t.Run("should fail if the application cleanup has not been requested", func(t *testing.T) {
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "uid"))
- uc := sut(&app)
-
- r, err := uc(ctx, delete_app.Command{
+ t.Run("should fail if the application cleanup has not been requested first", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ r, err := handler(context.Background(), delete_app.Command{
ID: string(app.ID()),
})
- testutil.ErrorIs(t, domain.ErrAppCleanupNeeded, err)
- testutil.Equals(t, bus.Unit, r)
+ assert.ErrorIs(t, domain.ErrAppCleanupNeeded, err)
+ assert.Equal(t, bus.Unit, r)
})
t.Run("should succeed if everything is good", func(t *testing.T) {
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "uid"))
- app.RequestCleanup("uid")
-
- uc := sut(&app)
-
- r, err := uc(ctx, delete_app.Command{
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ app.RequestCleanup(user.ID())
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ r, err := handler(context.Background(), delete_app.Command{
ID: string(app.ID()),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
- testutil.HasNEvents(t, &app, 3)
- testutil.EventIs[domain.AppDeleted](t, &app, 2)
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+ assert.HasLength(t, 1, dispatcher.Signals())
+
+ deleted := assert.Is[domain.AppDeleted](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.AppDeleted{
+ ID: app.ID(),
+ }, deleted)
})
}
diff --git a/internal/deployment/app/delete_registry/delete_registry_test.go b/internal/deployment/app/delete_registry/delete_registry_test.go
index f90ad4fa..8d881612 100644
--- a/internal/deployment/app/delete_registry/delete_registry_test.go
+++ b/internal/deployment/app/delete_registry/delete_registry_test.go
@@ -4,41 +4,51 @@ import (
"context"
"testing"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/delete_registry"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
)
func Test_DeleteRegistry(t *testing.T) {
- sut := func(existing ...*domain.Registry) bus.RequestHandler[bus.UnitType, delete_registry.Command] {
- store := memory.NewRegistriesStore(existing...)
- return delete_registry.Handler(store, store)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, delete_registry.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return delete_registry.Handler(context.RegistriesStore, context.RegistriesStore), context.Dispatcher
}
t.Run("should require an existing registry", func(t *testing.T) {
- uc := sut()
+ handler, _ := arrange(t)
- _, err := uc(context.Background(), delete_registry.Command{
+ _, err := handler(context.Background(), delete_registry.Command{
ID: "non-existing-id",
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
})
t.Run("should delete the registry", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- uc := sut(&r)
+ user := authfixture.User()
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithRegistries(®istry))
- _, err := uc(context.Background(), delete_registry.Command{
- ID: string(r.ID()),
+ _, err := handler(context.Background(), delete_registry.Command{
+ ID: string(registry.ID()),
})
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.RegistryDeleted](t, &r, 1)
- testutil.Equals(t, r.ID(), evt.ID)
+ assert.Nil(t, err)
+ assert.HasLength(t, 1, dispatcher.Signals())
+
+ deleted := assert.Is[domain.RegistryDeleted](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.RegistryDeleted{
+ ID: registry.ID(),
+ }, deleted)
})
}
diff --git a/internal/deployment/app/delete_target/delete_target_test.go b/internal/deployment/app/delete_target/delete_target_test.go
index 2c00fe9f..ce0f4c32 100644
--- a/internal/deployment/app/delete_target/delete_target_test.go
+++ b/internal/deployment/app/delete_target/delete_target_test.go
@@ -4,62 +4,71 @@ import (
"context"
"testing"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/delete_target"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
)
func Test_DeleteTarget(t *testing.T) {
- ctx := context.Background()
- sut := func(existingTargets ...*domain.Target) (bus.RequestHandler[bus.UnitType, delete_target.Command], *dummyProvider) {
- targetsStore := memory.NewTargetsStore(existingTargets...)
- provider := &dummyProvider{}
- return delete_target.Handler(targetsStore, targetsStore, provider), provider
+ arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, delete_target.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return delete_target.Handler(context.TargetsStore, context.TargetsStore, provider), context.Dispatcher
}
t.Run("should fail silently if the target does not exist anymore", func(t *testing.T) {
- uc, provider := sut()
+ var provider dummyProvider
+ handler, dispatcher := arrange(t, &provider)
- _, err := uc(ctx, delete_target.Command{})
+ _, err := handler(context.Background(), delete_target.Command{})
- testutil.IsNil(t, err)
- testutil.IsFalse(t, provider.called)
+ assert.Nil(t, err)
+ assert.False(t, provider.called)
+ assert.HasLength(t, 0, dispatcher.Signals())
})
t.Run("should fail if the target has not been requested for cleanup", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t, &provider, fixture.WithUsers(&user), fixture.WithTargets(&target))
- uc, provider := sut(&target)
-
- _, err := uc(ctx, delete_target.Command{
+ _, err := handler(context.Background(), delete_target.Command{
ID: string(target.ID()),
})
- testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err)
- testutil.IsFalse(t, provider.called)
+ assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, err)
+ assert.False(t, provider.called)
+ assert.HasLength(t, 0, dispatcher.Signals())
})
t.Run("should succeed if everything is good", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
+ var provider dummyProvider
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
- testutil.IsNil(t, target.RequestCleanup(false, "uid"))
-
- uc, provider := sut(&target)
+ assert.Nil(t, target.RequestCleanup(false, user.ID()))
+ handler, dispatcher := arrange(t, &provider, fixture.WithUsers(&user), fixture.WithTargets(&target))
- _, err := uc(ctx, delete_target.Command{
+ _, err := handler(context.Background(), delete_target.Command{
ID: string(target.ID()),
})
- testutil.IsNil(t, err)
- testutil.IsTrue(t, provider.called)
+ assert.Nil(t, err)
+ assert.True(t, provider.called)
+ assert.HasLength(t, 1, dispatcher.Signals())
+
+ deleted := assert.Is[domain.TargetDeleted](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetDeleted{
+ ID: target.ID(),
+ }, deleted)
})
}
diff --git a/internal/deployment/app/deploy/deploy_test.go b/internal/deployment/app/deploy/deploy_test.go
index 52fef77f..37ef978b 100644
--- a/internal/deployment/app/deploy/deploy_test.go
+++ b/internal/deployment/app/deploy/deploy_test.go
@@ -3,174 +3,228 @@ package deploy_test
import (
"context"
"errors"
- "os"
"testing"
- "github.com/YuukanOO/seelf/cmd/config"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/deploy"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/internal/deployment/infra/artifact"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
"github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
- "github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/log"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
-type initialData struct {
- deployments []*domain.Deployment
- targets []*domain.Target
-}
-
func Test_Deploy(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- logger, _ := log.NewLogger()
- sut := func(
+ arrange := func(
+ tb testing.TB,
source domain.Source,
provider domain.Provider,
- data initialData,
- ) bus.RequestHandler[bus.UnitType, deploy.Command] {
- opts := config.Default(config.WithTestDefaults())
- store := memory.NewDeploymentsStore(data.deployments...)
- targetsStore := memory.NewTargetsStore(data.targets...)
- registriesStore := memory.NewRegistriesStore()
- artifactManager := artifact.NewLocal(opts, logger)
-
- t.Cleanup(func() {
- os.RemoveAll(opts.DataDir())
- })
-
- return deploy.Handler(store, store, artifactManager, source, provider, targetsStore, registriesStore)
+ seed ...fixture.SeedBuilder,
+ ) (
+ bus.RequestHandler[bus.UnitType, deploy.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ logger, _ := log.NewLogger()
+ artifactManager := artifact.NewLocal(context.Config, logger)
+ return deploy.Handler(context.DeploymentsStore, context.DeploymentsStore, artifactManager, source, provider, context.TargetsStore, context.RegistriesStore), context.Context, context.Dispatcher
}
t.Run("should fail silently if the deployment does not exists", func(t *testing.T) {
- uc := sut(source(nil), provider(nil), initialData{})
- r, err := uc(ctx, deploy.Command{})
-
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
- })
+ handler, ctx, _ := arrange(t, source(nil), provider(nil))
- t.Run("should mark the deployment has failed if the target does not exist anymore", func(t *testing.T) {
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid"))
- src := source(nil)
- meta := must.Panic(src.Prepare(ctx, app, 42))
- depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid"))
+ r, err := handler(ctx, deploy.Command{})
- uc := sut(src, provider(nil), initialData{
- deployments: []*domain.Deployment{&depl},
- })
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+ })
- _, err := uc(ctx, deploy.Command{
- AppID: string(depl.ID().AppID()),
- DeploymentNumber: int(depl.ID().DeploymentNumber()),
+ t.Run("should mark the deployment has failed if the target is configuring", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ )
+ handler, ctx, dispatcher := arrange(t, source(nil), provider(nil),
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ _, err := handler(ctx, deploy.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
})
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2)
- testutil.Equals(t, apperr.ErrNotFound.Error(), evt.State.ErrCode().MustGet())
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ assert.HasLength(t, 0, dispatcher.Signals())
})
t.Run("should mark the deployment has failed if source does not succeed", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "some-uid"))
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
-
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "some-uid"))
- srcErr := errors.New("source_failed")
- src := source(srcErr)
- meta := must.Panic(src.Prepare(ctx, app, 42))
- depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid"))
- uc := sut(src, provider(nil), initialData{
- deployments: []*domain.Deployment{&depl},
- targets: []*domain.Target{&target},
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ )
+ sourceErr := errors.New("source_failed")
+ handler, ctx, dispatcher := arrange(t, source(sourceErr), provider(nil),
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ r, err := handler(ctx, deploy.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
})
- r, err := uc(ctx, deploy.Command{
- AppID: string(depl.ID().AppID()),
- DeploymentNumber: int(depl.ID().DeploymentNumber()),
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+
+ changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status())
+
+ changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.DeploymentStatusFailed, changed.State.Status())
+ assert.Equal(t, sourceErr.Error(), changed.State.ErrCode().MustGet())
+ })
+
+ t.Run("should mark the deployment has failed in the target is not correctly configured", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ target.Configured(target.CurrentVersion(), nil, errors.New("target_failed"))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ )
+ handler, ctx, dispatcher := arrange(t, source(nil), provider(nil),
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ r, err := handler(ctx, deploy.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2)
- testutil.IsTrue(t, evt.State.StartedAt().HasValue())
- testutil.IsTrue(t, evt.State.FinishedAt().HasValue())
- testutil.Equals(t, srcErr.Error(), evt.State.ErrCode().MustGet())
- testutil.Equals(t, domain.DeploymentStatusFailed, evt.State.Status())
+ changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status())
+
+ changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.DeploymentStatusFailed, changed.State.Status())
+ assert.Equal(t, domain.ErrTargetConfigurationFailed.Error(), changed.State.ErrCode().MustGet())
})
t.Run("should mark the deployment has failed if provider does not run the deployment successfully", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "some-uid"))
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
-
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "some-uid"))
- providerErr := errors.New("run_failed")
- be := provider(providerErr)
- src := source(nil)
- meta := must.Panic(src.Prepare(ctx, app, 42))
- depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid"))
- uc := sut(src, be, initialData{
- deployments: []*domain.Deployment{&depl},
- targets: []*domain.Target{&target},
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ )
+ providerErr := errors.New("provider_failed")
+ handler, ctx, dispatcher := arrange(t, source(nil), provider(providerErr),
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ r, err := handler(ctx, deploy.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
})
- r, err := uc(ctx, deploy.Command{
- AppID: string(depl.ID().AppID()),
- DeploymentNumber: int(depl.ID().DeploymentNumber()),
- })
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+
+ changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status())
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2)
- testutil.IsTrue(t, evt.State.StartedAt().HasValue())
- testutil.IsTrue(t, evt.State.FinishedAt().HasValue())
- testutil.Equals(t, providerErr.Error(), evt.State.ErrCode().MustGet())
- testutil.Equals(t, domain.DeploymentStatusFailed, evt.State.Status())
+ changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.DeploymentStatusFailed, changed.State.Status())
+ assert.Equal(t, providerErr.Error(), changed.State.ErrCode().MustGet())
})
t.Run("should mark the deployment has succeeded if all is good", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "some-uid"))
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
-
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "some-uid"))
- src := source(nil)
- meta := must.Panic(src.Prepare(ctx, app, 42))
- depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid"))
- uc := sut(src, provider(nil), initialData{
- deployments: []*domain.Deployment{&depl},
- targets: []*domain.Target{&target},
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ )
+ handler, ctx, dispatcher := arrange(t, source(nil), provider(nil),
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ r, err := handler(ctx, deploy.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
})
- r, err := uc(ctx, deploy.Command{
- AppID: string(depl.ID().AppID()),
- DeploymentNumber: int(depl.ID().DeploymentNumber()),
- })
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+
+ changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status())
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2)
- testutil.IsTrue(t, evt.State.StartedAt().HasValue())
- testutil.IsTrue(t, evt.State.FinishedAt().HasValue())
- testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status())
+ changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.DeploymentStatusSucceeded, changed.State.Status())
})
}
diff --git a/internal/deployment/app/expose_seelf_container/expose_seelf_container.go b/internal/deployment/app/expose_seelf_container/expose_seelf_container.go
index 004bd130..f8658ea3 100644
--- a/internal/deployment/app/expose_seelf_container/expose_seelf_container.go
+++ b/internal/deployment/app/expose_seelf_container/expose_seelf_container.go
@@ -78,7 +78,6 @@ func Handler(
}
target, err = domain.NewTarget("local",
- urlRequirement,
configRequirement,
auth.CurrentUser(ctx).MustGet(),
)
@@ -87,6 +86,10 @@ func Handler(
return bus.Unit, err
}
+ if err = target.ExposeServicesAutomatically(urlRequirement); err != nil {
+ return bus.Unit, err
+ }
+
assigned, err := provider.Setup(ctx, target)
target.Configured(target.CurrentVersion(), assigned, err)
diff --git a/internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go b/internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go
index 0c6e6fdb..a3f77e51 100644
--- a/internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go
+++ b/internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go
@@ -11,7 +11,7 @@ import (
// When an app is about to be deleted, cancel all pending deployments
func OnAppCleanupRequestedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[domain.AppCleanupRequested] {
return func(ctx context.Context, evt domain.AppCleanupRequested) error {
- return writer.FailDeployments(ctx, domain.ErrAppCleanupRequested, domain.FailCriterias{
+ return writer.FailDeployments(ctx, domain.ErrAppCleanupRequested, domain.FailCriteria{
Status: monad.Value(domain.DeploymentStatusPending),
App: monad.Value(evt.ID),
})
diff --git a/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go b/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go
index 993a38f0..ec68f2eb 100644
--- a/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go
+++ b/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go
@@ -14,7 +14,7 @@ func OnAppEnvChangedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[d
return nil
}
- return writer.FailDeployments(ctx, domain.ErrAppTargetChanged, domain.FailCriterias{
+ return writer.FailDeployments(ctx, domain.ErrAppTargetChanged, domain.FailCriteria{
Status: monad.Value(domain.DeploymentStatusPending),
App: monad.Value(evt.ID),
Environment: monad.Value(evt.Environment),
diff --git a/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go b/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go
index 5209f7d3..1e04ac5d 100644
--- a/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go
+++ b/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go
@@ -10,7 +10,7 @@ import (
func OnTargetDeleteRequestedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[domain.TargetCleanupRequested] {
return func(ctx context.Context, evt domain.TargetCleanupRequested) error {
- return writer.FailDeployments(ctx, domain.ErrTargetCleanupRequested, domain.FailCriterias{
+ return writer.FailDeployments(ctx, domain.ErrTargetCleanupRequested, domain.FailCriteria{
Status: monad.Value(domain.DeploymentStatusPending),
Target: monad.Value(evt.ID),
})
diff --git a/internal/deployment/app/get_deployment/get_deployment_test.go b/internal/deployment/app/get_deployment/get_deployment_test.go
index 871ac981..4ac0ac53 100644
--- a/internal/deployment/app/get_deployment/get_deployment_test.go
+++ b/internal/deployment/app/get_deployment/get_deployment_test.go
@@ -4,8 +4,8 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/app/get_deployment"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/monad"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Deployment(t *testing.T) {
@@ -54,7 +54,7 @@ func Test_Deployment(t *testing.T) {
d.ResolveServicesUrls()
- testutil.DeepEquals(t, get_deployment.Services{
+ assert.DeepEqual(t, get_deployment.Services{
{
Name: "app",
Image: "app-image",
@@ -112,7 +112,7 @@ func Test_Deployment(t *testing.T) {
d.ResolveServicesUrls()
- testutil.DeepEquals(t, get_deployment.Services{
+ assert.DeepEqual(t, get_deployment.Services{
{
Name: "app",
Image: "app-image",
@@ -152,7 +152,7 @@ func Test_Deployment(t *testing.T) {
d.ResolveServicesUrls()
- testutil.DeepEquals(t, get_deployment.Services{
+ assert.DeepEqual(t, get_deployment.Services{
{
Name: "app",
Image: "app-image",
@@ -233,7 +233,7 @@ func Test_Deployment(t *testing.T) {
d.ResolveServicesUrls()
- testutil.DeepEquals(t, get_deployment.Services{
+ assert.DeepEqual(t, get_deployment.Services{
{
Name: "app",
Image: "app-image",
diff --git a/internal/deployment/app/get_target/get_target.go b/internal/deployment/app/get_target/get_target.go
index 542b7775..74a50379 100644
--- a/internal/deployment/app/get_target/get_target.go
+++ b/internal/deployment/app/get_target/get_target.go
@@ -22,7 +22,7 @@ type (
Target struct {
ID string `json:"id"`
Name string `json:"name"`
- Url string `json:"url"`
+ Url monad.Maybe[string] `json:"url"`
Provider Provider `json:"provider"`
State State `json:"state"`
CleanupRequestedAt monad.Maybe[time.Time] `json:"cleanup_requested_at"`
diff --git a/internal/deployment/app/group.go b/internal/deployment/app/group.go
index 63b7ff46..76fc27c6 100644
--- a/internal/deployment/app/group.go
+++ b/internal/deployment/app/group.go
@@ -6,7 +6,7 @@ import (
// Group for deployment to prevent multiple deployment at the same time on the same
// environment.
-func DeploymentGroup(config domain.DeploymentConfig) string {
+func DeploymentGroup(config domain.ConfigSnapshot) string {
return "deployment.deployment.deploy." + config.ProjectName()
}
diff --git a/internal/deployment/app/promote/promote_test.go b/internal/deployment/app/promote/promote_test.go
index b42c3131..74f976d5 100644
--- a/internal/deployment/app/promote/promote_test.go
+++ b/internal/deployment/app/promote/promote_test.go
@@ -4,60 +4,131 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/promote"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
- "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
)
func Test_Promote(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid"))
- appsStore := memory.NewAppsStore(&app)
-
- sut := func(existingDeployments ...*domain.Deployment) bus.RequestHandler[int, promote.Command] {
- deploymentsStore := memory.NewDeploymentsStore(existingDeployments...)
- return promote.Handler(appsStore, deploymentsStore, deploymentsStore)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[int, promote.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return promote.Handler(context.AppsStore, context.DeploymentsStore, context.DeploymentsStore), context.Context, context.Dispatcher
}
t.Run("should fail if application does not exist", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, promote.Command{
+ handler, ctx, _ := arrange(t)
+
+ num, err := handler(ctx, promote.Command{
AppID: "some-app-id",
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
- testutil.Equals(t, 0, num)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.Zero(t, num)
})
t.Run("should fail if source deployment does not exist", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, promote.Command{
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ num, err := handler(ctx, promote.Command{
AppID: string(app.ID()),
DeploymentNumber: 1,
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
- testutil.Equals(t, 0, num)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.Zero(t, num)
+ })
+
+ t.Run("should returns an err if trying to promote a production deployment", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
+
+ number, err := handler(ctx, promote.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
+ })
+
+ assert.ErrorIs(t, domain.ErrCouldNotPromoteProductionDeployment, err)
+ assert.Zero(t, number)
})
t.Run("should correctly creates a new deployment based on the provided one", func(t *testing.T) {
- dpl, _ := app.NewDeployment(1, raw.Data(""), domain.Staging, "some-uid")
- uc := sut(&dpl)
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Staging),
+ )
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
- number, err := uc(ctx, promote.Command{
- AppID: string(dpl.ID().AppID()),
- DeploymentNumber: int(dpl.ID().DeploymentNumber()),
+ number, err := handler(ctx, promote.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, 2, number)
+ assert.Nil(t, err)
+ assert.Equal(t, 2, number)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ created := assert.Is[domain.DeploymentCreated](t, dispatcher.Signals()[0])
+ assert.DeepEqual(t, domain.DeploymentCreated{
+ ID: domain.DeploymentIDFrom(app.ID(), 2),
+ Config: created.Config,
+ State: created.State,
+ Source: deployment.Source(),
+ Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Requested.At())),
+ }, created)
})
}
diff --git a/internal/deployment/app/query.go b/internal/deployment/app/query.go
index 98f0cd46..eb7f82b8 100644
--- a/internal/deployment/app/query.go
+++ b/internal/deployment/app/query.go
@@ -9,9 +9,9 @@ type (
}
TargetSummary struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Url string `json:"url"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Url monad.Maybe[string] `json:"url"`
}
LatestDeployments[T any] struct {
diff --git a/internal/deployment/app/queue_deployment/queue_deployment_test.go b/internal/deployment/app/queue_deployment/queue_deployment_test.go
index c0fdeb47..31bfced0 100644
--- a/internal/deployment/app/queue_deployment/queue_deployment_test.go
+++ b/internal/deployment/app/queue_deployment/queue_deployment_test.go
@@ -4,75 +4,125 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/queue_deployment"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
"github.com/YuukanOO/seelf/pkg/validate"
)
func Test_QueueDeployment(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid"))
- appsStore := memory.NewAppsStore(&app)
-
- sut := func() bus.RequestHandler[int, queue_deployment.Command] {
- deploymentsStore := memory.NewDeploymentsStore()
- return queue_deployment.Handler(appsStore, deploymentsStore, deploymentsStore, raw.New())
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[int, queue_deployment.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return queue_deployment.Handler(context.AppsStore, context.DeploymentsStore, context.DeploymentsStore, raw.New()), context.Context, context.Dispatcher
}
- t.Run("should fail if payload is empty", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, queue_deployment.Command{
- AppID: string(app.ID()),
+ t.Run("should fail if the app does not exist", func(t *testing.T) {
+ handler, ctx, _ := arrange(t)
+
+ num, err := handler(ctx, queue_deployment.Command{
+ AppID: "does-not-exist",
Environment: "production",
})
- testutil.ErrorIs(t, domain.ErrInvalidSourcePayload, err)
- testutil.Equals(t, 0, num)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.Zero(t, num)
})
- t.Run("should fail if no environment has been given", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, queue_deployment.Command{
- AppID: string(app.ID()),
- })
+ t.Run("should fail if payload is empty", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- testutil.Equals(t, 0, num)
+ num, err := handler(ctx, queue_deployment.Command{
+ AppID: string(app.ID()),
+ Environment: "production",
+ })
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, validationErr["environment"])
+ assert.ErrorIs(t, domain.ErrInvalidSourcePayload, err)
+ assert.Zero(t, num)
})
- t.Run("should fail if the app does not exist", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, queue_deployment.Command{
- AppID: "does-not-exist",
- Environment: "production",
+ t.Run("should fail if no environment has been given", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ num, err := handler(ctx, queue_deployment.Command{
+ AppID: string(app.ID()),
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
- testutil.Equals(t, 0, num)
+ assert.Zero(t, num)
+ assert.ValidationError(t, validate.FieldErrors{
+ "environment": domain.ErrInvalidEnvironmentName,
+ }, err)
})
t.Run("should succeed if everything is good", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, queue_deployment.Command{
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ num, err := handler(ctx, queue_deployment.Command{
AppID: string(app.ID()),
Environment: "production",
Source: "some-payload",
})
- testutil.IsNil(t, err)
- testutil.Equals(t, 1, num)
+ assert.Nil(t, err)
+ assert.Equal(t, 1, num)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ created := assert.Is[domain.DeploymentCreated](t, dispatcher.Signals()[0])
+ assert.DeepEqual(t, domain.DeploymentCreated{
+ ID: domain.DeploymentIDFrom(app.ID(), 1),
+ Config: created.Config,
+ State: created.State,
+ Source: raw.Data("some-payload"),
+ Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Requested.At())),
+ }, created)
})
}
diff --git a/internal/deployment/app/reconfigure_target/reconfigure_target_test.go b/internal/deployment/app/reconfigure_target/reconfigure_target_test.go
index cb8e1015..0fb97d01 100644
--- a/internal/deployment/app/reconfigure_target/reconfigure_target_test.go
+++ b/internal/deployment/app/reconfigure_target/reconfigure_target_test.go
@@ -4,44 +4,73 @@ import (
"context"
"testing"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/reconfigure_target"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
)
func Test_ReconfigureTarget(t *testing.T) {
- sut := func(existingTargets ...*domain.Target) bus.RequestHandler[bus.UnitType, reconfigure_target.Command] {
- store := memory.NewTargetsStore(existingTargets...)
- return reconfigure_target.Handler(store, store)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, reconfigure_target.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return reconfigure_target.Handler(context.TargetsStore, context.TargetsStore), context.Dispatcher
}
- t.Run("should returns an err if the target does not exist", func(t *testing.T) {
- uc := sut()
+ t.Run("should returns an error if the target does not exist", func(t *testing.T) {
+ handler, _ := arrange(t)
+
+ _, err := handler(context.Background(), reconfigure_target.Command{})
+
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ })
- _, err := uc(context.Background(), reconfigure_target.Command{})
+ t.Run("should returns an error if the target is already being configured", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, _ := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target))
+
+ _, err := handler(context.Background(), reconfigure_target.Command{
+ ID: string(target.ID()),
+ })
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
})
- t.Run("should force the reconfiguration of the target", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(nil, true), "uid"))
+ t.Run("should returns an error if the target is being deleted", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, user.ID()))
+ handler, _ := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target))
- uc := sut(&target)
+ _, err := handler(context.Background(), reconfigure_target.Command{
+ ID: string(target.ID()),
+ })
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, err)
+ })
+
+ t.Run("should reconfigure the target if everything is good", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ target.Configured(target.CurrentVersion(), nil, nil)
+ handler, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target))
- _, err := uc(context.Background(), reconfigure_target.Command{
+ _, err := handler(context.Background(), reconfigure_target.Command{
ID: string(target.ID()),
})
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &target, 3)
- changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 2)
- testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status())
+ assert.Nil(t, err)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ changed := assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status())
})
}
diff --git a/internal/deployment/app/redeploy/on_app_env_changed.go b/internal/deployment/app/redeploy/on_app_env_changed.go
index cf878622..64ce5b53 100644
--- a/internal/deployment/app/redeploy/on_app_env_changed.go
+++ b/internal/deployment/app/redeploy/on_app_env_changed.go
@@ -39,7 +39,7 @@ func OnAppEnvChangedHandler(
return err
}
- depl, err := app.Redeploy(source, number, auth.CurrentUser(ctx).MustGet())
+ deployment, err := app.Redeploy(source, number, auth.CurrentUser(ctx).MustGet())
// Could not redeploy the latest deployment, maybe because of a configuration change,
// just skip it (for example, trying to redeploy a git deployment but the vcs is now missing)
@@ -47,6 +47,6 @@ func OnAppEnvChangedHandler(
return nil
}
- return writer.Write(ctx, &depl)
+ return writer.Write(ctx, &deployment)
}
}
diff --git a/internal/deployment/app/redeploy/redeploy_test.go b/internal/deployment/app/redeploy/redeploy_test.go
index cb1c4691..417dfe67 100644
--- a/internal/deployment/app/redeploy/redeploy_test.go
+++ b/internal/deployment/app/redeploy/redeploy_test.go
@@ -4,61 +4,101 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/redeploy"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
- "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
)
func Test_Redeploy(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid"))
- appsStore := memory.NewAppsStore(&app)
- sut := func(existingDeployments ...*domain.Deployment) bus.RequestHandler[int, redeploy.Command] {
- deploymentsStore := memory.NewDeploymentsStore(existingDeployments...)
- return redeploy.Handler(appsStore, deploymentsStore, deploymentsStore)
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[int, redeploy.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return redeploy.Handler(context.AppsStore, context.DeploymentsStore, context.DeploymentsStore), context.Context, context.Dispatcher
}
- t.Run("should fail if application does not exist", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, redeploy.Command{
+ t.Run("should fail if the application does not exist", func(t *testing.T) {
+ handler, ctx, _ := arrange(t)
+
+ num, err := handler(ctx, redeploy.Command{
AppID: "some-app-id",
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
- testutil.Equals(t, 0, num)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.Zero(t, num)
})
t.Run("should fail if source deployment does not exist", func(t *testing.T) {
- uc := sut()
- num, err := uc(ctx, redeploy.Command{
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ num, err := handler(ctx, redeploy.Command{
AppID: string(app.ID()),
DeploymentNumber: 1,
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
- testutil.Equals(t, 0, num)
-
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.Zero(t, num)
})
t.Run("should correctly creates a new deployment based on the provided one", func(t *testing.T) {
- dpl, _ := app.NewDeployment(1, raw.Data(""), domain.Production, "some-uid")
- uc := sut(&dpl)
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ )
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithDeployments(&deployment),
+ )
- num, err := uc(ctx, redeploy.Command{
- AppID: string(dpl.ID().AppID()),
- DeploymentNumber: int(dpl.ID().DeploymentNumber()),
+ num, err := handler(ctx, redeploy.Command{
+ AppID: string(deployment.ID().AppID()),
+ DeploymentNumber: int(deployment.ID().DeploymentNumber()),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, 2, num)
+ assert.Nil(t, err)
+ assert.Equal(t, 2, num)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ created := assert.Is[domain.DeploymentCreated](t, dispatcher.Signals()[0])
+ assert.DeepEqual(t, domain.DeploymentCreated{
+ ID: domain.DeploymentIDFrom(app.ID(), 2),
+ Config: created.Config,
+ State: created.State,
+ Source: deployment.Source(),
+ Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Requested.At())),
+ }, created)
})
}
diff --git a/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go b/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go
index 981fda82..a09c59b0 100644
--- a/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go
+++ b/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go
@@ -4,47 +4,69 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/request_app_cleanup"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
)
func Test_RequestAppCleanup(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- sut := func(existingApps ...*domain.App) bus.RequestHandler[bus.UnitType, request_app_cleanup.Command] {
- store := memory.NewAppsStore(existingApps...)
- return request_app_cleanup.Handler(store, store)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, request_app_cleanup.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return request_app_cleanup.Handler(context.AppsStore, context.AppsStore), context.Context, context.Dispatcher
}
t.Run("should fail if the application does not exist", func(t *testing.T) {
- uc := sut()
+ handler, ctx, _ := arrange(t)
- r, err := uc(ctx, request_app_cleanup.Command{
+ r, err := handler(ctx, request_app_cleanup.Command{
ID: "some-id",
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
- testutil.Equals(t, bus.Unit, r)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.Equal(t, bus.Unit, r)
})
t.Run("should mark an application has ready for deletion", func(t *testing.T) {
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid"))
- uc := sut(&app)
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
- r, err := uc(ctx, request_app_cleanup.Command{
+ r, err := handler(ctx, request_app_cleanup.Command{
ID: string(app.ID()),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, bus.Unit, r)
-
- testutil.EventIs[domain.AppCleanupRequested](t, &app, 1)
+ assert.Nil(t, err)
+ assert.Equal(t, bus.Unit, r)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ requested := assert.Is[domain.AppCleanupRequested](t, dispatcher.Signals()[0])
+ assert.DeepEqual(t, domain.AppCleanupRequested{
+ ID: app.ID(),
+ ProductionConfig: requested.ProductionConfig,
+ StagingConfig: requested.StagingConfig,
+ Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, requested.Requested.At())),
+ }, requested)
})
}
diff --git a/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go b/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go
index 21196df6..e8db202d 100644
--- a/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go
+++ b/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go
@@ -4,82 +4,96 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/request_target_cleanup"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
)
-type initialData struct {
- targets []*domain.Target
- apps []*domain.App
-}
-
func Test_RequestTargetCleanup(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- sut := func(existing initialData) bus.RequestHandler[bus.UnitType, request_target_cleanup.Command] {
- targetsStore := memory.NewTargetsStore(existing.targets...)
- appsStore := memory.NewAppsStore(existing.apps...)
- return request_target_cleanup.Handler(targetsStore, targetsStore, appsStore)
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[bus.UnitType, request_target_cleanup.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return request_target_cleanup.Handler(context.TargetsStore, context.TargetsStore, context.AppsStore), context.Context, context.Dispatcher
}
t.Run("should returns an error if the target does not exist", func(t *testing.T) {
- uc := sut(initialData{})
+ handler, ctx, _ := arrange(t)
- _, err := uc(ctx, request_target_cleanup.Command{
+ _, err := handler(ctx, request_target_cleanup.Command{
ID: "some-id",
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
})
t.Run("should returns an error if the target has still apps using it", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true),
- domain.NewProviderConfigRequirement(dummyProviderConfig{}, true), "uid"))
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "uid"))
-
- uc := sut(initialData{
- targets: []*domain.Target{&target},
- apps: []*domain.App{&app},
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ _, err := handler(ctx, request_target_cleanup.Command{
+ ID: string(target.ID()),
})
- _, err := uc(ctx, request_target_cleanup.Command{
+ assert.ErrorIs(t, domain.ErrTargetInUse, err)
+ })
+
+ t.Run("should returns an error if the target is configuring", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(ctx, request_target_cleanup.Command{
ID: string(target.ID()),
})
- testutil.ErrorIs(t, domain.ErrTargetInUse, err)
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
})
t.Run("should correctly mark the target for cleanup", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true),
- domain.NewProviderConfigRequirement(dummyProviderConfig{}, true), "uid"))
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
target.Configured(target.CurrentVersion(), nil, nil)
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
- uc := sut(initialData{
- targets: []*domain.Target{&target},
- })
-
- _, err := uc(ctx, request_target_cleanup.Command{
+ _, err := handler(ctx, request_target_cleanup.Command{
ID: string(target.ID()),
})
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &target, 3)
- evt := testutil.EventIs[domain.TargetCleanupRequested](t, &target, 2)
- testutil.Equals(t, target.ID(), evt.ID)
+ assert.Nil(t, err)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ requested := assert.Is[domain.TargetCleanupRequested](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetCleanupRequested{
+ ID: target.ID(),
+ Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, requested.Requested.At())),
+ }, requested)
})
}
-
-type dummyProviderConfig struct {
- domain.ProviderConfig
-}
diff --git a/internal/deployment/app/update_app/update_app_test.go b/internal/deployment/app/update_app/update_app_test.go
index ab8d6c53..576ce916 100644
--- a/internal/deployment/app/update_app/update_app_test.go
+++ b/internal/deployment/app/update_app/update_app_test.go
@@ -4,317 +4,425 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/update_app"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/YuukanOO/seelf/pkg/validate"
)
func Test_UpdateApp(t *testing.T) {
- production := domain.NewEnvironmentConfig("1")
- production.HasEnvironmentVariables(domain.ServicesEnv{"app": {"DEBUG": "false"}})
- staging := domain.NewEnvironmentConfig("1")
- staging.HasEnvironmentVariables(domain.ServicesEnv{"app": {"DEBUG": "false"}})
- ctx := auth.WithUserID(context.Background(), "some-uid")
-
- sut := func(existingApps ...*domain.App) bus.RequestHandler[string, update_app.Command] {
- store := memory.NewAppsStore(existingApps...)
- return update_app.Handler(store, store)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, update_app.Command],
+ context.Context,
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return update_app.Handler(context.AppsStore, context.AppsStore), context.Context, context.Dispatcher
}
t.Run("should require a valid application id", func(t *testing.T) {
- uc := sut()
- id, err := uc(ctx, update_app.Command{})
+ handler, ctx, _ := arrange(t)
+
+ id, err := handler(ctx, update_app.Command{})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
- testutil.Equals(t, "", id)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.Zero(t, id)
})
t.Run("should update nothing if no fields are provided", func(t *testing.T) {
- a := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid"))
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ id, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(a.ID()), id)
- testutil.HasNEvents(t, &a, 1)
+ assert.Nil(t, err)
+ assert.Equal(t, string(app.ID()), id)
+ assert.HasLength(t, 0, dispatcher.Signals())
})
t.Run("should validate new target naming availability", func(t *testing.T) {
- a1 := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("2"), true, true), "some-uid"))
- a2 := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("3"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("4"), true, true), "some-uid"))
- uc := sut(&a1, &a2)
-
- _, err := uc(ctx, update_app.Command{
- ID: string(a2.ID()),
+ user := authfixture.User()
+ targetOne := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ targetTwo := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ appOne := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithAppName("my-app"),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(targetOne.ID()),
+ domain.NewEnvironmentConfig(targetOne.ID()),
+ ),
+ )
+ appTwo := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithAppName("my-app"),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(targetTwo.ID()),
+ domain.NewEnvironmentConfig(targetTwo.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&targetOne, &targetTwo),
+ fixture.WithApps(&appOne, &appTwo),
+ )
+
+ _, err := handler(ctx, update_app.Command{
+ ID: string(appTwo.ID()),
Production: monad.Value(update_app.EnvironmentConfig{
- Target: "1",
+ Target: string(targetOne.ID()),
}),
Staging: monad.Value(update_app.EnvironmentConfig{
- Target: "2",
+ Target: string(targetOne.ID()),
}),
})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["production.target"])
- testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["staging.target"])
+ assert.ValidationError(t, validate.FieldErrors{
+ "production.target": domain.ErrAppNameAlreadyTaken,
+ "staging.target": domain.ErrAppNameAlreadyTaken,
+ }, err)
})
t.Run("should remove an application env variables", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
-
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ otherTarget := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ configWithEnvVariables := domain.NewEnvironmentConfig(target.ID())
+ configWithEnvVariables.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": {"DEBUG": "false"},
+ })
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ configWithEnvVariables,
+ configWithEnvVariables,
+ ),
+ )
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target, &otherTarget),
+ fixture.WithApps(&app),
+ )
+
+ id, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
Production: monad.Value(update_app.EnvironmentConfig{
- Target: "new-production-target",
+ Target: string(otherTarget.ID()),
}),
Staging: monad.Value(update_app.EnvironmentConfig{
- Target: "new-staging-target",
+ Target: string(otherTarget.ID()),
}),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(a.ID()), id)
- testutil.HasNEvents(t, &a, 3)
-
- evt := testutil.EventIs[domain.AppEnvChanged](t, &a, 1)
+ assert.Nil(t, err)
+ assert.Equal(t, string(app.ID()), id)
+ assert.HasLength(t, 2, dispatcher.Signals())
- testutil.Equals(t, domain.Production, evt.Environment)
- testutil.Equals(t, "new-production-target", evt.Config.Target())
- testutil.IsFalse(t, evt.Config.Vars().HasValue())
+ changed := assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.Production, changed.Environment)
+ assert.Equal(t, otherTarget.ID(), changed.Config.Target())
+ assert.False(t, changed.Config.Vars().HasValue())
- evt = testutil.EventIs[domain.AppEnvChanged](t, &a, 2)
-
- testutil.Equals(t, domain.Staging, evt.Environment)
- testutil.Equals(t, "new-staging-target", evt.Config.Target())
- testutil.IsFalse(t, evt.Config.Vars().HasValue())
+ changed = assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.Staging, changed.Environment)
+ assert.Equal(t, otherTarget.ID(), changed.Config.Target())
+ assert.False(t, changed.Config.Vars().HasValue())
})
t.Run("should update an application env variables", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
-
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ configWithEnvVariables := domain.NewEnvironmentConfig(target.ID())
+ configWithEnvVariables.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": {"DEBUG": "false"},
+ })
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ configWithEnvVariables,
+ configWithEnvVariables,
+ ),
+ )
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ id, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
Production: monad.Value(update_app.EnvironmentConfig{
- Target: "new-production-target",
+ Target: string(target.ID()),
Vars: monad.Value(map[string]map[string]string{
"app": {"OTHER": "value"},
}),
}),
Staging: monad.Value(update_app.EnvironmentConfig{
- Target: "new-staging-target",
+ Target: string(target.ID()),
Vars: monad.Value(map[string]map[string]string{
"app": {"SOMETHING": "else"},
}),
}),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(a.ID()), id)
- testutil.HasNEvents(t, &a, 3)
+ assert.Nil(t, err)
+ assert.Equal(t, string(app.ID()), id)
+ assert.HasLength(t, 2, dispatcher.Signals())
- evt := testutil.EventIs[domain.AppEnvChanged](t, &a, 1)
-
- testutil.Equals(t, domain.Production, evt.Environment)
- testutil.Equals(t, "new-production-target", evt.Config.Target())
- testutil.DeepEquals(t, domain.ServicesEnv{
+ changed := assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.Production, changed.Environment)
+ assert.Equal(t, target.ID(), changed.Config.Target())
+ assert.DeepEqual(t, domain.ServicesEnv{
"app": {"OTHER": "value"},
- }, evt.Config.Vars().MustGet())
-
- evt = testutil.EventIs[domain.AppEnvChanged](t, &a, 2)
+ }, changed.Config.Vars().MustGet())
- testutil.Equals(t, domain.Staging, evt.Environment)
- testutil.Equals(t, "new-staging-target", evt.Config.Target())
- testutil.DeepEquals(t, domain.ServicesEnv{
+ changed = assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.Staging, changed.Environment)
+ assert.Equal(t, target.ID(), changed.Config.Target())
+ assert.DeepEqual(t, domain.ServicesEnv{
"app": {"SOMETHING": "else"},
- }, evt.Config.Vars().MustGet())
+ }, changed.Config.Vars().MustGet())
})
t.Run("should require valid vcs inputs", func(t *testing.T) {
- uc := sut()
- id, err := uc(ctx, update_app.Command{
- ID: "an-app",
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ _, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
VersionControl: monad.PatchValue(update_app.VersionControl{
Url: "invalid-url",
}),
})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- testutil.Equals(t, "", id)
+ assert.ValidationError(t, validate.FieldErrors{
+ "version_control.url": domain.ErrInvalidUrl,
+ }, err)
})
t.Run("should fail if trying to update an app being deleted", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
- a.RequestCleanup("uid")
-
- uc := sut(&a)
-
- _, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ app.RequestCleanup(user.ID())
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ _, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
VersionControl: monad.PatchValue(update_app.VersionControl{
Url: "https://some.url",
}),
})
- testutil.ErrorIs(t, domain.ErrAppCleanupRequested, err)
+ assert.ErrorIs(t, domain.ErrAppCleanupRequested, err)
})
t.Run("should fail if trying to add a vcs config without an url defined", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
-
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ handler, ctx, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ _, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
VersionControl: monad.PatchValue(update_app.VersionControl{}),
})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- testutil.Equals(t, "", id)
+ assert.ValidationError(t, validate.FieldErrors{
+ "version_control.url": domain.ErrInvalidUrl,
+ }, err)
})
t.Run("should remove the vcs config if nil given", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
- url := must.Panic(domain.UrlFrom("https://some.url"))
- a.UseVersionControl(domain.NewVersionControl(url))
-
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ assert.Nil(t, app.UseVersionControl(domain.NewVersionControl(must.Panic(domain.UrlFrom("https://some.url")))))
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ id, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
VersionControl: monad.Nil[update_app.VersionControl](),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(a.ID()), id)
- testutil.HasNEvents(t, &a, 3)
- testutil.EventIs[domain.AppVersionControlRemoved](t, &a, 2)
+ assert.Nil(t, err)
+ assert.Equal(t, string(app.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ removed := assert.Is[domain.AppVersionControlRemoved](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.AppVersionControlRemoved{
+ ID: app.ID(),
+ }, removed)
})
- t.Run("should update the vcs url", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
- url := must.Panic(domain.UrlFrom("https://some.url"))
- vcs := domain.NewVersionControl(url)
+ t.Run("should update the vcs url and keep the token if defined", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ vcs := domain.NewVersionControl(must.Panic(domain.UrlFrom("https://some.url")))
vcs.Authenticated("a token")
- a.UseVersionControl(vcs)
-
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ assert.Nil(t, app.UseVersionControl(vcs))
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ id, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
VersionControl: monad.PatchValue(update_app.VersionControl{
Url: "https://some.other.url",
}),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(a.ID()), id)
- testutil.HasNEvents(t, &a, 3)
- evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &a, 2)
- testutil.Equals(t, "https://some.other.url", evt.Config.Url().String())
- testutil.Equals(t, "a token", evt.Config.Token().MustGet())
+ assert.Nil(t, err)
+ assert.Equal(t, string(app.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ configured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[0])
+ assert.Equal(t, app.ID(), configured.ID)
+ assert.Equal(t, "https://some.other.url", configured.Config.Url().String())
+ assert.Equal(t, "a token", configured.Config.Token().MustGet())
})
t.Run("should remove the vcs token", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
url := must.Panic(domain.UrlFrom("https://some.url"))
vcs := domain.NewVersionControl(url)
vcs.Authenticated("a token")
- a.UseVersionControl(vcs)
-
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ assert.Nil(t, app.UseVersionControl(vcs))
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ id, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
VersionControl: monad.PatchValue(update_app.VersionControl{
Url: "https://some.url",
Token: monad.Nil[string](),
}),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(a.ID()), id)
- testutil.HasNEvents(t, &a, 3)
- evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &a, 2)
- testutil.Equals(t, "https://some.url", evt.Config.Url().String())
- testutil.IsFalse(t, evt.Config.Token().HasValue())
+ assert.Nil(t, err)
+ assert.Equal(t, string(app.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ configured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[0])
+ assert.Equal(t, app.ID(), configured.ID)
+ assert.Equal(t, url, configured.Config.Url())
+ assert.False(t, configured.Config.Token().HasValue())
})
t.Run("should update the vcs token", func(t *testing.T) {
- a := must.Panic(domain.NewApp("an-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid",
- ))
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ app := fixture.App(
+ fixture.WithAppCreatedBy(user.ID()),
+ fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
url := must.Panic(domain.UrlFrom("https://some.url"))
vcs := domain.NewVersionControl(url)
vcs.Authenticated("a token")
- a.UseVersionControl(vcs)
-
- uc := sut(&a)
-
- id, err := uc(ctx, update_app.Command{
- ID: string(a.ID()),
+ assert.Nil(t, app.UseVersionControl(vcs))
+ handler, ctx, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ )
+
+ id, err := handler(ctx, update_app.Command{
+ ID: string(app.ID()),
VersionControl: monad.PatchValue(update_app.VersionControl{
Url: "https://some.url",
Token: monad.PatchValue("new token"),
}),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(a.ID()), id)
- testutil.HasNEvents(t, &a, 3)
- evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &a, 2)
- testutil.Equals(t, "https://some.url", evt.Config.Url().String())
- testutil.Equals(t, "new token", evt.Config.Token().Get(""))
+ assert.Nil(t, err)
+ assert.Equal(t, string(app.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ configured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[0])
+ assert.Equal(t, app.ID(), configured.ID)
+ assert.Equal(t, url, configured.Config.Url())
+ assert.Equal(t, "new token", configured.Config.Token().Get(""))
})
}
diff --git a/internal/deployment/app/update_registry/update_registry_test.go b/internal/deployment/app/update_registry/update_registry_test.go
index eb7812a0..3d4daafd 100644
--- a/internal/deployment/app/update_registry/update_registry_test.go
+++ b/internal/deployment/app/update_registry/update_registry_test.go
@@ -4,149 +4,188 @@ import (
"context"
"testing"
- auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/update_registry"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/YuukanOO/seelf/pkg/validate"
)
func Test_UpdateRegistry(t *testing.T) {
- ctx := auth.WithUserID(context.Background(), "some-uid")
- sut := func(existing ...*domain.Registry) bus.RequestHandler[string, update_registry.Command] {
- store := memory.NewRegistriesStore(existing...)
- return update_registry.Handler(store, store)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, update_registry.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return update_registry.Handler(context.RegistriesStore, context.RegistriesStore), context.Dispatcher
}
t.Run("should require valid inputs", func(t *testing.T) {
- uc := sut()
+ handler, _ := arrange(t)
- id, err := uc(ctx, update_registry.Command{
+ _, err := handler(context.Background(), update_registry.Command{
Url: monad.Value("not an url"),
})
- testutil.Equals(t, "", id)
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrInvalidUrl, validationErr["url"])
+ assert.ValidationError(t, validate.FieldErrors{
+ "url": domain.ErrInvalidUrl,
+ }, err)
})
t.Run("should require an existing registry", func(t *testing.T) {
- uc := sut()
+ handler, _ := arrange(t)
- _, err := uc(ctx, update_registry.Command{
+ _, err := handler(context.Background(), update_registry.Command{
Url: monad.Value("http://example.com"),
})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
})
t.Run("should rename a registry", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- uc := sut(&r)
+ user := authfixture.User()
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithRegistries(®istry))
- id, err := uc(ctx, update_registry.Command{
- ID: string(r.ID()),
+ id, err := handler(context.Background(), update_registry.Command{
+ ID: string(registry.ID()),
Name: monad.Value("new-name"),
})
- testutil.NotEquals(t, "", id)
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.RegistryRenamed](t, &r, 1)
- testutil.Equals(t, r.ID(), evt.ID)
- testutil.Equals(t, "new-name", evt.Name)
+ assert.Nil(t, err)
+ assert.Equal(t, string(registry.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ renamed := assert.Is[domain.RegistryRenamed](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.RegistryRenamed{
+ ID: registry.ID(),
+ Name: "new-name",
+ }, renamed)
})
t.Run("should require a unique url when updating it", func(t *testing.T) {
- r1 := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- r2 := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://localhost:5000")), true), "uid"))
- uc := sut(&r1, &r2)
-
- id, err := uc(ctx, update_registry.Command{
- ID: string(r2.ID()),
+ user := authfixture.User()
+ registry := fixture.Registry(
+ fixture.WithRegistryCreatedBy(user.ID()),
+ fixture.WithUrl(must.Panic(domain.UrlFrom("http://example.com"))),
+ )
+ otherRegistry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ handler, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithRegistries(®istry, &otherRegistry),
+ )
+
+ _, err := handler(context.Background(), update_registry.Command{
+ ID: string(otherRegistry.ID()),
Url: monad.Value("http://example.com"),
})
- testutil.Equals(t, "", id)
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validationErr["url"])
+ assert.ValidationError(t, validate.FieldErrors{
+ "url": domain.ErrUrlAlreadyTaken,
+ }, err)
})
t.Run("should update the url if its good", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- uc := sut(&r)
-
- id, err := uc(ctx, update_registry.Command{
- ID: string(r.ID()),
+ user := authfixture.User()
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithRegistries(®istry),
+ )
+
+ id, err := handler(context.Background(), update_registry.Command{
+ ID: string(registry.ID()),
Url: monad.Value("http://localhost:5000"),
})
- testutil.NotEquals(t, "", id)
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.RegistryUrlChanged](t, &r, 1)
- testutil.Equals(t, r.ID(), evt.ID)
- testutil.Equals(t, "http://localhost:5000", evt.Url.String())
+ assert.Nil(t, err)
+ assert.Equal(t, string(registry.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ changed := assert.Is[domain.RegistryUrlChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.RegistryUrlChanged{
+ ID: registry.ID(),
+ Url: must.Panic(domain.UrlFrom("http://localhost:5000")),
+ }, changed)
})
t.Run("should be able to add credentials", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- uc := sut(&r)
-
- id, err := uc(ctx, update_registry.Command{
- ID: string(r.ID()),
+ user := authfixture.User()
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithRegistries(®istry),
+ )
+
+ id, err := handler(context.Background(), update_registry.Command{
+ ID: string(registry.ID()),
Credentials: monad.PatchValue(update_registry.Credentials{
Username: "user",
Password: monad.Value("password"),
}),
})
- testutil.NotEquals(t, "", id)
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.RegistryCredentialsChanged](t, &r, 1)
- testutil.Equals(t, r.ID(), evt.ID)
- testutil.Equals(t, "user", evt.Credentials.Username())
- testutil.Equals(t, "password", evt.Credentials.Password())
+ assert.Nil(t, err)
+ assert.Equal(t, string(registry.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ changed := assert.Is[domain.RegistryCredentialsChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.RegistryCredentialsChanged{
+ ID: registry.ID(),
+ Credentials: domain.NewCredentials("user", "password"),
+ }, changed)
})
t.Run("should be able to update only the credentials username", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- r.UseAuthentication(domain.NewCredentials("user", "password"))
- uc := sut(&r)
-
- id, err := uc(ctx, update_registry.Command{
- ID: string(r.ID()),
+ user := authfixture.User()
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ registry.UseAuthentication(domain.NewCredentials("user", "password"))
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithRegistries(®istry),
+ )
+
+ id, err := handler(context.Background(), update_registry.Command{
+ ID: string(registry.ID()),
Credentials: monad.PatchValue(update_registry.Credentials{
Username: "new-user",
}),
})
- testutil.NotEquals(t, "", id)
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.RegistryCredentialsChanged](t, &r, 2)
- testutil.Equals(t, r.ID(), evt.ID)
- testutil.Equals(t, "new-user", evt.Credentials.Username())
- testutil.Equals(t, "password", evt.Credentials.Password())
+ assert.Nil(t, err)
+ assert.Equal(t, string(registry.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ changed := assert.Is[domain.RegistryCredentialsChanged](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.RegistryCredentialsChanged{
+ ID: registry.ID(),
+ Credentials: domain.NewCredentials("new-user", "password"),
+ }, changed)
})
t.Run("should be able to remove authentication", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
- r.UseAuthentication(domain.NewCredentials("user", "password"))
- uc := sut(&r)
-
- id, err := uc(ctx, update_registry.Command{
- ID: string(r.ID()),
+ user := authfixture.User()
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ registry.UseAuthentication(domain.NewCredentials("user", "password"))
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithRegistries(®istry),
+ )
+
+ id, err := handler(context.Background(), update_registry.Command{
+ ID: string(registry.ID()),
Credentials: monad.Nil[update_registry.Credentials](),
})
- testutil.NotEquals(t, "", id)
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.RegistryCredentialsRemoved](t, &r, 2)
- testutil.Equals(t, r.ID(), evt.ID)
+ assert.Nil(t, err)
+ assert.Equal(t, string(registry.ID()), id)
+ assert.HasLength(t, 1, dispatcher.Signals())
+ removed := assert.Is[domain.RegistryCredentialsRemoved](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.RegistryCredentialsRemoved{
+ ID: registry.ID(),
+ }, removed)
})
}
diff --git a/internal/deployment/app/update_target/update_target.go b/internal/deployment/app/update_target/update_target.go
index c8e3fa8d..e99c34cd 100644
--- a/internal/deployment/app/update_target/update_target.go
+++ b/internal/deployment/app/update_target/update_target.go
@@ -15,7 +15,7 @@ type Command struct {
ID string `json:"-"`
Name monad.Maybe[string] `json:"name"`
- Url monad.Maybe[string] `json:"url"`
+ Url monad.Patch[string] `json:"url"`
Provider any `json:"-"`
}
@@ -31,7 +31,7 @@ func Handler(
if err := validate.Struct(validate.Of{
"name": validate.Maybe(cmd.Name, strings.Required),
- "url": validate.Maybe(cmd.Url, func(s string) error {
+ "url": validate.Patch(cmd.Url, func(s string) error {
return validate.Value(s, &targetUrl, domain.UrlFrom)
}),
}); err != nil {
@@ -87,8 +87,14 @@ func Handler(
}
}
- if cmd.Url.HasValue() {
- if err = target.HasUrl(urlRequirement); err != nil {
+ if cmd.Url.IsSet() {
+ if cmd.Url.HasValue() {
+ err = target.ExposeServicesAutomatically(urlRequirement)
+ } else {
+ err = target.ExposeServicesManually()
+ }
+
+ if err != nil {
return "", err
}
}
diff --git a/internal/deployment/app/update_target/update_target_test.go b/internal/deployment/app/update_target/update_target_test.go
index 44a93780..28d29f29 100644
--- a/internal/deployment/app/update_target/update_target_test.go
+++ b/internal/deployment/app/update_target/update_target_test.go
@@ -4,97 +4,142 @@ import (
"context"
"testing"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
"github.com/YuukanOO/seelf/internal/deployment/app/update_target"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/internal/deployment/infra/memory"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/YuukanOO/seelf/pkg/validate"
)
func Test_UpdateTarget(t *testing.T) {
- sut := func(existingTargets ...*domain.Target) bus.RequestHandler[string, update_target.Command] {
- store := memory.NewTargetsStore(existingTargets...)
- provider := &dummyProvider{}
- return update_target.Handler(store, store, provider)
+
+ arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) (
+ bus.RequestHandler[string, update_target.Command],
+ spy.Dispatcher,
+ ) {
+ context := fixture.PrepareDatabase(tb, seed...)
+ return update_target.Handler(context.TargetsStore, context.TargetsStore, &dummyProvider{}), context.Dispatcher
}
t.Run("should fail if the target does not exist", func(t *testing.T) {
- uc := sut()
+ handler, _ := arrange(t)
- _, err := uc(context.Background(), update_target.Command{})
+ _, err := handler(context.Background(), update_target.Command{})
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
})
t.Run("should fail if url or config are already taken", func(t *testing.T) {
- t1 := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(dummyConfig{"1"}, true), "uid"))
- t2 := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true),
- domain.NewProviderConfigRequirement(dummyConfig{"2"}, true), "uid"))
- uc := sut(&t1, &t2)
-
- _, err := uc(context.Background(), update_target.Command{
- ID: string(t1.ID()),
- Provider: "2",
- Url: monad.Value("http://docker.localhost"),
+ user := authfixture.User()
+ config := fixture.ProviderConfig()
+ targetOne := fixture.Target(
+ fixture.WithTargetCreatedBy(user.ID()),
+ fixture.WithProviderConfig(config),
+ )
+ assert.Nil(t, targetOne.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true)))
+ targetTwo := fixture.Target(
+ fixture.WithTargetCreatedBy(user.ID()),
+ )
+ assert.Nil(t, targetTwo.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ handler, _ := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&targetOne, &targetTwo),
+ )
+
+ _, err := handler(context.Background(), update_target.Command{
+ ID: string(targetTwo.ID()),
+ Provider: config,
+ Url: monad.PatchValue("http://localhost"),
})
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, validationErr["dummy"])
- testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validationErr["url"])
+ assert.ValidationError(t, validate.FieldErrors{
+ "url": domain.ErrUrlAlreadyTaken,
+ config.Kind(): domain.ErrConfigAlreadyTaken,
+ }, err)
})
- t.Run("should update the target if everything is good", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true),
- domain.NewProviderConfigRequirement(dummyConfig{"1"}, true), "uid"))
- uc := sut(&target)
+ t.Run("should be able to remove the url", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(
+ fixture.WithTargetCreatedBy(user.ID()),
+ fixture.WithProviderConfig(fixture.ProviderConfig(fixture.WithFingerprint("test"))),
+ )
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(context.Background(), update_target.Command{
+ ID: string(target.ID()),
+ Url: monad.Nil[string](),
+ })
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 2, dispatcher.Signals())
+ urlRemoved := assert.Is[domain.TargetUrlRemoved](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetUrlRemoved{
+ ID: target.ID(),
+ }, urlRemoved)
+ })
- id, err := uc(context.Background(), update_target.Command{
+ t.Run("should update the target if everything is good", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(
+ fixture.WithTargetCreatedBy(user.ID()),
+ fixture.WithProviderConfig(fixture.ProviderConfig(fixture.WithFingerprint("test"), fixture.WithKind("test"))),
+ )
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+ newConfig := fixture.ProviderConfig(fixture.WithFingerprint("test"), fixture.WithKind("test"))
+
+ id, err := handler(context.Background(), update_target.Command{
ID: string(target.ID()),
Name: monad.Value("new name"),
- Provider: "1",
- Url: monad.Value("http://docker.localhost"),
+ Provider: newConfig,
+ Url: monad.PatchValue("http://docker.localhost"),
})
- testutil.IsNil(t, err)
- testutil.Equals(t, string(target.ID()), id)
- testutil.HasNEvents(t, &target, 6)
-
- renamed := testutil.EventIs[domain.TargetRenamed](t, &target, 1)
- testutil.Equals(t, "new name", renamed.Name)
- urlChanged := testutil.EventIs[domain.TargetUrlChanged](t, &target, 2)
- testutil.Equals(t, "http://docker.localhost", urlChanged.Url.String())
- providerChanged := testutil.EventIs[domain.TargetProviderChanged](t, &target, 4)
- testutil.Equals(t, domain.ProviderConfig(dummyConfig{"1"}), providerChanged.Provider)
- testutil.EventIs[domain.TargetStateChanged](t, &target, 3)
- testutil.EventIs[domain.TargetStateChanged](t, &target, 5)
+ assert.Nil(t, err)
+ assert.Equal(t, string(target.ID()), id)
+ assert.HasLength(t, 5, dispatcher.Signals())
+
+ renamed := assert.Is[domain.TargetRenamed](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetRenamed{
+ ID: target.ID(),
+ Name: "new name",
+ }, renamed)
+
+ urlChanged := assert.Is[domain.TargetUrlChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.TargetUrlChanged{
+ ID: target.ID(),
+ Url: must.Panic(domain.UrlFrom("http://docker.localhost")),
+ }, urlChanged)
+
+ assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[2])
+
+ providerChanged := assert.Is[domain.TargetProviderChanged](t, dispatcher.Signals()[3])
+ assert.Equal(t, domain.TargetProviderChanged{
+ ID: target.ID(),
+ Provider: newConfig,
+ }, providerChanged)
+
+ assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[4])
})
}
-type (
- dummyProvider struct {
- domain.Provider
- }
-
- dummyConfig struct {
- data string
- }
-)
+type dummyProvider struct {
+ domain.Provider
+}
func (*dummyProvider) Prepare(ctx context.Context, payload any, existing ...domain.ProviderConfig) (domain.ProviderConfig, error) {
- return dummyConfig{payload.(string)}, nil
+ return payload.(domain.ProviderConfig), nil
}
-
-func (dummyConfig) Kind() string { return "dummy" }
-func (c dummyConfig) Fingerprint() string { return c.data }
-func (c dummyConfig) Equals(other domain.ProviderConfig) bool { return false }
-func (c dummyConfig) String() string { return c.data }
diff --git a/internal/deployment/domain/app.go b/internal/deployment/domain/app.go
index aebf6dc4..5413bf35 100644
--- a/internal/deployment/domain/app.go
+++ b/internal/deployment/domain/app.go
@@ -238,12 +238,12 @@ func (a *App) RemoveVersionControl() error {
// Updates the production configuration for this application.
func (a *App) HasProductionConfig(configRequirement EnvironmentConfigRequirement) error {
- return a.tryUpdateEnvironmentConfig(Production, configRequirement)
+ return a.tryUpdateEnvironmentConfig(Production, a.production, configRequirement)
}
// Updates the staging configuration for this application.
func (a *App) HasStagingConfig(configRequirement EnvironmentConfigRequirement) error {
- return a.tryUpdateEnvironmentConfig(Staging, configRequirement)
+ return a.tryUpdateEnvironmentConfig(Staging, a.staging, configRequirement)
}
// Request cleaning for this application. This marks the application for deletion.
@@ -275,28 +275,16 @@ func (a *App) Delete(cleanedUp bool) error {
func (a *App) ID() AppID { return a.id }
func (a *App) VersionControl() monad.Maybe[VersionControl] { return a.versionControl }
-func (a *App) Production() EnvironmentConfig { return a.production }
-func (a *App) Staging() EnvironmentConfig { return a.staging }
func (a *App) tryUpdateEnvironmentConfig(
env Environment,
+ existingConfig EnvironmentConfig,
updatedConfigRequirement EnvironmentConfigRequirement,
) error {
if a.cleanupRequested.HasValue() {
return ErrAppCleanupRequested
}
- var existingConfig EnvironmentConfig
-
- switch env {
- case Production:
- existingConfig = a.production
- case Staging:
- existingConfig = a.staging
- default:
- return ErrInvalidEnvironmentName
- }
-
updatedConfig, err := updatedConfigRequirement.Met()
if err != nil {
@@ -308,10 +296,7 @@ func (a *App) tryUpdateEnvironmentConfig(
return nil
}
- // Same target, does not update the inner version
- if updatedConfig.target == existingConfig.target {
- updatedConfig.version = existingConfig.version
- }
+ updatedConfig.consolidate(existingConfig)
a.apply(AppEnvChanged{
ID: a.id,
diff --git a/internal/deployment/domain/app_test.go b/internal/deployment/domain/app_test.go
index 762b31a5..f43ce746 100644
--- a/internal/deployment/domain/app_test.go
+++ b/internal/deployment/domain/app_test.go
@@ -5,22 +5,23 @@ import (
auth "github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_App(t *testing.T) {
- var (
- appname domain.AppName = "my-app"
- uid auth.UserID = "uid"
- production = domain.NewEnvironmentConfig("production-target")
- staging = domain.NewEnvironmentConfig("staging-target")
- productionAvailable = domain.NewEnvironmentConfigRequirement(production, true, true)
- stagingAvailable = domain.NewEnvironmentConfigRequirement(staging, true, true)
- )
t.Run("should require a unique name across both target environments", func(t *testing.T) {
+ var (
+ appname domain.AppName = "my-app"
+ uid auth.UserID = "uid"
+ production = domain.NewEnvironmentConfig("production-target")
+ staging = domain.NewEnvironmentConfig("staging-target")
+ )
+
tests := []struct {
production domain.EnvironmentConfigRequirement
staging domain.EnvironmentConfigRequirement
@@ -51,196 +52,227 @@ func Test_App(t *testing.T) {
for _, test := range tests {
_, err := domain.NewApp(appname, test.production, test.staging, uid)
- testutil.ErrorIs(t, test.expected, err)
+ assert.ErrorIs(t, test.expected, err)
}
})
t.Run("should correctly creates a new app", func(t *testing.T) {
- app, err := domain.NewApp(appname, productionAvailable, stagingAvailable, uid)
-
- testutil.IsNil(t, err)
- testutil.NotEquals(t, "", app.ID())
- testutil.IsFalse(t, app.VersionControl().HasValue())
-
- evt := testutil.EventIs[domain.AppCreated](t, &app, 0)
-
- testutil.Equals(t, app.ID(), evt.ID)
- testutil.Equals(t, evt.Created.By(), uid)
- testutil.IsFalse(t, evt.Created.At().IsZero())
- testutil.IsTrue(t, evt.Production.Equals(production))
- testutil.IsTrue(t, evt.Staging.Equals(staging))
- testutil.Equals(t, appname, evt.Name)
+ var (
+ appname domain.AppName = "my-app"
+ uid auth.UserID = "uid"
+ production = domain.NewEnvironmentConfig("production-target")
+ staging = domain.NewEnvironmentConfig("staging-target")
+ )
+
+ app, err := domain.NewApp(appname,
+ domain.NewEnvironmentConfigRequirement(production, true, true),
+ domain.NewEnvironmentConfigRequirement(staging, true, true),
+ uid)
+
+ assert.Nil(t, err)
+ assert.NotZero(t, app.ID())
+ assert.False(t, app.VersionControl().HasValue())
+
+ evt := assert.EventIs[domain.AppCreated](t, &app, 0)
+
+ assert.DeepEqual(t, domain.AppCreated{
+ ID: app.ID(),
+ Name: appname,
+ Created: shared.ActionFrom(uid, assert.NotZero(t, evt.Created.At())),
+ Production: production,
+ Staging: staging,
+ }, evt)
})
t.Run("could have a vcs config attached", func(t *testing.T) {
- url := must.Panic(domain.UrlFrom("http://somewhere.com"))
- vcsConfig := domain.NewVersionControl(url)
+ vcsConfig := domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com")))
vcsConfig.Authenticated("vcskey")
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
- app.UseVersionControl(vcsConfig)
+ app := fixture.App()
+
+ err := app.UseVersionControl(vcsConfig)
- testutil.Equals(t, vcsConfig, app.VersionControl().MustGet())
- testutil.HasNEvents(t, &app, 2)
- evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &app, 1)
- testutil.Equals(t, app.ID(), evt.ID)
- testutil.Equals(t, vcsConfig, evt.Config)
+ assert.Nil(t, err)
+ assert.Equal(t, vcsConfig, app.VersionControl().MustGet())
+ assert.HasNEvents(t, 2, &app)
+ evt := assert.EventIs[domain.AppVersionControlConfigured](t, &app, 1)
+
+ assert.Equal(t, domain.AppVersionControlConfigured{
+ ID: app.ID(),
+ Config: vcsConfig,
+ }, evt)
})
t.Run("could have a vcs config removed", func(t *testing.T) {
- url := must.Panic(domain.UrlFrom("http://somewhere.com"))
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
- app.RemoveVersionControl()
+ app := fixture.App()
- testutil.HasNEvents(t, &app, 1)
+ assert.Nil(t, app.RemoveVersionControl())
+ assert.HasNEvents(t, 1, &app, "should have nothing new since it didn't have a vcs config initially")
- app.UseVersionControl(domain.NewVersionControl(url))
- app.RemoveVersionControl()
+ assert.Nil(t, app.UseVersionControl(domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com")))))
+ assert.Nil(t, app.RemoveVersionControl())
- testutil.HasNEvents(t, &app, 3)
- testutil.EventIs[domain.AppVersionControlRemoved](t, &app, 2)
+ assert.HasNEvents(t, 3, &app, "should have 2 new events, one for the config added and one for the config removed")
+ assert.EventIs[domain.AppVersionControlRemoved](t, &app, 2)
})
t.Run("raise a VCS configured event only if configs are different", func(t *testing.T) {
- url := must.Panic(domain.UrlFrom("http://somewhere.com"))
- vcsConfig := domain.NewVersionControl(url)
- vcsConfig.Authenticated("vcskey")
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
- app.UseVersionControl(vcsConfig)
- app.UseVersionControl(vcsConfig)
-
- testutil.HasNEvents(t, &app, 2)
-
- anotherUrl, _ := domain.UrlFrom("http://somewhere.else.com")
- otherConfig := domain.NewVersionControl(anotherUrl)
- app.UseVersionControl(otherConfig)
- testutil.HasNEvents(t, &app, 3)
- evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &app, 2)
- testutil.Equals(t, otherConfig, evt.Config)
+ vcsConfig := domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com")))
+ app := fixture.App()
+
+ assert.Nil(t, app.UseVersionControl(vcsConfig))
+ assert.Nil(t, app.UseVersionControl(vcsConfig))
+
+ assert.HasNEvents(t, 2, &app, "should raise an event only once since the configs are equal")
+
+ otherConfig := domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.else.com")))
+ assert.Nil(t, app.UseVersionControl(otherConfig))
+
+ assert.HasNEvents(t, 3, &app, "should raise an event since configs are different")
+ evt := assert.EventIs[domain.AppVersionControlConfigured](t, &app, 2)
+
+ assert.Equal(t, domain.AppVersionControlConfigured{
+ ID: app.ID(),
+ Config: otherConfig,
+ }, evt)
})
t.Run("does not allow to modify the vcs config if the app is marked for deletion", func(t *testing.T) {
- url := must.Panic(domain.UrlFrom("http://somewhere.com"))
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ app := fixture.App()
app.RequestCleanup("uid")
- testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.UseVersionControl(domain.NewVersionControl(url)))
- testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.RemoveVersionControl())
+ assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.UseVersionControl(
+ domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com")))))
+ assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.RemoveVersionControl())
})
t.Run("need the app naming to be available when modifying a configuration", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ app := fixture.App()
- err := app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(staging, false, false))
+ err := app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("another-target"), false, false))
- testutil.ErrorIs(t, apperr.ErrNotFound, err)
+ assert.ErrorIs(t, apperr.ErrNotFound, err)
})
t.Run("should update the environment config version only if target has changed", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ config := domain.NewEnvironmentConfig("production-target")
+ app := fixture.App(fixture.WithEnvironmentConfig(config, config))
- newConfig := domain.NewEnvironmentConfig(production.Target())
+ newConfig := domain.NewEnvironmentConfig(config.Target())
newConfig.HasEnvironmentVariables(domain.ServicesEnv{
"app": {"DEBUG": "another value"},
})
- err := app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))
+ assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)))
+ changed := assert.EventIs[domain.AppEnvChanged](t, &app, 1)
- testutil.IsNil(t, err)
- testutil.Equals(t, production.Version(), app.Production().Version())
+ assert.Equal(t, changed.OldConfig.Version(), changed.Config.Version(), "same target should keep the same version")
newConfig = domain.NewEnvironmentConfig("another-target")
- err = app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))
+ assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)))
+ changed = assert.EventIs[domain.AppEnvChanged](t, &app, 2)
- testutil.IsNil(t, err)
- testutil.NotEquals(t, production.Version(), app.Production().Version())
- testutil.Equals(t, newConfig.Version(), app.Production().Version())
+ assert.NotEqual(t, changed.OldConfig.Version(), changed.Config.Version())
+ assert.Equal(t, newConfig.Version(), changed.Config.Version(), "should match the new config version")
})
t.Run("raise an env changed event only if the new config is different", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ production := domain.NewEnvironmentConfig("production-target")
+ staging := domain.NewEnvironmentConfig("staging-target")
+ app := fixture.App(fixture.WithEnvironmentConfig(production, staging))
- errProd := app.HasProductionConfig(productionAvailable)
- errStaging := app.HasStagingConfig(stagingAvailable)
+ assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(production, true, true)))
+ assert.Nil(t, app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(staging, true, true)))
- testutil.IsNil(t, errProd)
- testutil.IsNil(t, errStaging)
- testutil.HasNEvents(t, &app, 1)
+ assert.HasNEvents(t, 1, &app, "same configs should not trigger new events")
newConfig := domain.NewEnvironmentConfig("new-target")
newConfig.HasEnvironmentVariables(domain.ServicesEnv{
"app": {"DEBUG": "true"},
})
- errProd = app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))
- errStaging = app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))
-
- testutil.IsNil(t, errProd)
- testutil.IsNil(t, errStaging)
- testutil.HasNEvents(t, &app, 3)
- evt := testutil.EventIs[domain.AppEnvChanged](t, &app, 1)
-
- testutil.Equals(t, app.ID(), evt.ID)
- testutil.Equals(t, domain.Production, evt.Environment)
- testutil.DeepEquals(t, newConfig, evt.Config)
+ assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)))
+ assert.Nil(t, app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)))
- evt = testutil.EventIs[domain.AppEnvChanged](t, &app, 2)
+ assert.HasNEvents(t, 3, &app, "new configs should trigger new events")
+ changed := assert.EventIs[domain.AppEnvChanged](t, &app, 1)
- testutil.Equals(t, app.ID(), evt.ID)
- testutil.Equals(t, domain.Staging, evt.Environment)
- testutil.DeepEquals(t, newConfig, evt.Config)
+ assert.DeepEqual(t, domain.AppEnvChanged{
+ ID: app.ID(),
+ Environment: domain.Production,
+ Config: newConfig,
+ OldConfig: production,
+ }, changed)
+
+ changed = assert.EventIs[domain.AppEnvChanged](t, &app, 2)
+
+ assert.DeepEqual(t, domain.AppEnvChanged{
+ ID: app.ID(),
+ Environment: domain.Staging,
+ Config: newConfig,
+ OldConfig: staging,
+ }, changed)
})
t.Run("does not allow to modify the environment config if the app is marked for deletion", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ app := fixture.App()
app.RequestCleanup("uid")
- testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasProductionConfig(productionAvailable))
- testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasStagingConfig(stagingAvailable))
+ assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("another-target"), true, true)))
+ assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("another-target"), true, true)))
})
t.Run("could be marked for deletion only if not already the case", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ production := domain.NewEnvironmentConfig("production-target")
+ staging := domain.NewEnvironmentConfig("staging-target")
+ app := fixture.App(fixture.WithEnvironmentConfig(production, staging))
app.RequestCleanup("uid")
app.RequestCleanup("uid")
- testutil.HasNEvents(t, &app, 2)
- evt := testutil.EventIs[domain.AppCleanupRequested](t, &app, 1)
- testutil.Equals(t, app.ID(), evt.ID)
- testutil.Equals(t, "uid", evt.Requested.By())
+ assert.HasNEvents(t, 2, &app, "should raise the event once")
+ evt := assert.EventIs[domain.AppCleanupRequested](t, &app, 1)
+
+ assert.DeepEqual(t, domain.AppCleanupRequested{
+ ID: app.ID(),
+ ProductionConfig: production,
+ StagingConfig: staging,
+ Requested: shared.ActionFrom[auth.UserID]("uid", evt.Requested.At()),
+ }, evt)
})
t.Run("should not allow a deletion if app resources have not been cleaned up", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
-
+ app := fixture.App()
app.RequestCleanup("uid")
err := app.Delete(false)
- testutil.ErrorIs(t, domain.ErrAppCleanupNeeded, err)
- testutil.HasNEvents(t, &app, 2)
+ assert.ErrorIs(t, domain.ErrAppCleanupNeeded, err)
+ assert.HasNEvents(t, 2, &app)
})
t.Run("raise an error if delete is called for a non cleaned up app", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ app := fixture.App()
err := app.Delete(false)
- testutil.ErrorIs(t, domain.ErrAppCleanupNeeded, err)
+ assert.ErrorIs(t, domain.ErrAppCleanupNeeded, err)
})
t.Run("could be deleted", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ app := fixture.App()
app.RequestCleanup("uid")
err := app.Delete(true)
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &app, 3)
- evt := testutil.EventIs[domain.AppDeleted](t, &app, 2)
- testutil.Equals(t, app.ID(), evt.ID)
+ assert.Nil(t, err)
+ assert.HasNEvents(t, 3, &app)
+ evt := assert.EventIs[domain.AppDeleted](t, &app, 2)
+
+ assert.Equal(t, domain.AppDeleted{
+ ID: app.ID(),
+ }, evt)
})
}
@@ -253,10 +285,10 @@ func Test_AppEvents(t *testing.T) {
OldConfig: domain.NewEnvironmentConfig("target"),
}
- testutil.IsFalse(t, evt.TargetHasChanged())
+ assert.False(t, evt.TargetHasChanged())
evt.OldConfig = domain.NewEnvironmentConfig("another-target")
- testutil.IsTrue(t, evt.TargetHasChanged())
+ assert.True(t, evt.TargetHasChanged())
})
}
diff --git a/internal/deployment/domain/appname_test.go b/internal/deployment/domain/appname_test.go
index 2e7fa2fe..6b5f0231 100644
--- a/internal/deployment/domain/appname_test.go
+++ b/internal/deployment/domain/appname_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_AppNameFrom(t *testing.T) {
@@ -26,15 +26,15 @@ func Test_AppNameFrom(t *testing.T) {
}
for _, test := range tests {
- t.Run("", func(t *testing.T) {
+ t.Run(test.input, func(t *testing.T) {
r, err := domain.AppNameFrom(test.input)
if test.valid {
- testutil.Equals(t, domain.AppName(test.input), r)
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
+ assert.Equal(t, domain.AppName(test.input), r)
} else {
- testutil.Equals(t, "", r)
- testutil.ErrorIs(t, domain.ErrInvalidAppName, err)
+ assert.ErrorIs(t, domain.ErrInvalidAppName, err)
+ assert.Equal(t, "", r)
}
})
}
diff --git a/internal/deployment/domain/deployment_config.go b/internal/deployment/domain/config_snapshot.go
similarity index 64%
rename from internal/deployment/domain/deployment_config.go
rename to internal/deployment/domain/config_snapshot.go
index 2dee38ec..49070776 100644
--- a/internal/deployment/domain/deployment_config.go
+++ b/internal/deployment/domain/config_snapshot.go
@@ -7,9 +7,9 @@ import (
)
// Holds data related to the configuration of the final application. It should
-// have everything needed to resolve service and image names and is the primarly used
+// have everything needed to resolve service and image names and is the primarily used
// structure during the deployment by a provider.
-type DeploymentConfig struct {
+type ConfigSnapshot struct {
appid AppID
appname AppName
environment Environment
@@ -18,10 +18,10 @@ type DeploymentConfig struct {
}
// Builds a new config snapshot for the given environment.
-func (a *App) ConfigSnapshotFor(env Environment) (DeploymentConfig, error) {
+func (a *App) configSnapshotFor(env Environment) (ConfigSnapshot, error) {
var (
conf EnvironmentConfig
- snapshot DeploymentConfig
+ snapshot ConfigSnapshot
)
switch env {
@@ -42,15 +42,15 @@ func (a *App) ConfigSnapshotFor(env Environment) (DeploymentConfig, error) {
return snapshot, nil
}
-func (c DeploymentConfig) AppID() AppID { return c.appid }
-func (c DeploymentConfig) AppName() AppName { return c.appname }
-func (c DeploymentConfig) Environment() Environment { return c.environment }
-func (c DeploymentConfig) Target() TargetID { return c.target }
-func (c DeploymentConfig) Vars() monad.Maybe[ServicesEnv] { return c.vars } // FIXME: If I want to follow my mantra, it should returns a readonly map
+func (c ConfigSnapshot) AppID() AppID { return c.appid }
+func (c ConfigSnapshot) AppName() AppName { return c.appname }
+func (c ConfigSnapshot) Environment() Environment { return c.environment }
+func (c ConfigSnapshot) Target() TargetID { return c.target }
+func (c ConfigSnapshot) Vars() monad.Maybe[ServicesEnv] { return c.vars } // FIXME: If I want to follow my mantra, it should returns a readonly map
// Retrieve environment variables associated with the given service name.
// FIXME: If I want to follow my mantra, it should returns a readonly map
-func (c DeploymentConfig) EnvironmentVariablesFor(service string) (m monad.Maybe[EnvVars]) {
+func (c ConfigSnapshot) EnvironmentVariablesFor(service string) (m monad.Maybe[EnvVars]) {
env, isSet := c.vars.TryGet()
if !isSet {
@@ -68,8 +68,14 @@ func (c DeploymentConfig) EnvironmentVariablesFor(service string) (m monad.Maybe
return m
}
+// Retrieve the name of the project which is the combination of the appname, environment and appid
+// targeted by this configuration.
+func (c ConfigSnapshot) ProjectName() string {
+ return string(c.appname) + "-" + string(c.environment) + "-" + strings.ToLower(string(c.appid))
+}
+
// Returns the subdomain that will be used to expose a specific service.
-func (c DeploymentConfig) SubDomain(service string, isDefault bool) string {
+func (c ConfigSnapshot) subDomain(service string, isDefault bool) string {
subdomain := string(c.appname)
if !c.environment.IsProduction() {
@@ -86,17 +92,11 @@ func (c DeploymentConfig) SubDomain(service string, isDefault bool) string {
}
// Builds a unique image name for the given service.
-func (c DeploymentConfig) ImageName(service string) string {
+func (c ConfigSnapshot) imageName(service string) string {
return string(c.appname) + "-" + strings.ToLower(string(c.appid)) + "/" + service + ":" + string(c.environment)
}
// Builds a qualified name, truly unique, for the given service.
-func (c DeploymentConfig) QualifiedName(service string) string {
+func (c ConfigSnapshot) qualifiedName(service string) string {
return c.ProjectName() + "-" + service
}
-
-// Retrieve the name of the project wich is the combination of the appname, environment and appid
-// targeted by this configuration.
-func (c DeploymentConfig) ProjectName() string {
- return string(c.appname) + "-" + string(c.environment) + "-" + strings.ToLower(string(c.appid))
-}
diff --git a/internal/deployment/domain/config_snapshot_test.go b/internal/deployment/domain/config_snapshot_test.go
new file mode 100644
index 00000000..9928071f
--- /dev/null
+++ b/internal/deployment/domain/config_snapshot_test.go
@@ -0,0 +1,66 @@
+package domain_test
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+)
+
+func Test_ConfigSnapshot(t *testing.T) {
+
+ t.Run("could be created", func(t *testing.T) {
+ config := domain.NewEnvironmentConfig("production-target")
+ config.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": {"DEBUG": "false"},
+ "db": {"USERNAME": "prodadmin"},
+ })
+
+ app := fixture.App(fixture.WithAppName("my-app"), fixture.WithProductionConfig(config))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ conf := deployment.Config()
+
+ assert.Equal(t, app.ID(), conf.AppID())
+ assert.Equal(t, "my-app", conf.AppName())
+ assert.Equal(t, domain.Production, conf.Environment())
+ assert.Equal(t, config.Target(), conf.Target())
+ assert.DeepEqual(t, config.Vars(), conf.Vars())
+ })
+
+ t.Run("should provide a way to retrieve environment variables for a service name", func(t *testing.T) {
+ config := domain.NewEnvironmentConfig("production-target")
+ config.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": {"DEBUG": "false"},
+ "db": {"USERNAME": "prodadmin"},
+ })
+
+ app := fixture.App(fixture.WithAppName("my-app"), fixture.WithProductionConfig(config))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ conf := deployment.Config()
+
+ assert.False(t, conf.EnvironmentVariablesFor("otherservice").HasValue())
+ assert.True(t, conf.EnvironmentVariablesFor("app").HasValue())
+ assert.DeepEqual(t, domain.EnvVars{
+ "DEBUG": "false",
+ }, conf.EnvironmentVariablesFor("app").MustGet())
+ })
+
+ t.Run("should return an empty monad if no environment variables are defined at all", func(t *testing.T) {
+ app := fixture.App()
+ deployment := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Staging))
+ conf := deployment.Config()
+
+ assert.False(t, conf.EnvironmentVariablesFor("app").HasValue())
+ })
+
+ t.Run("should expose a unique project name", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ deployment := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Staging))
+ conf := deployment.Config()
+
+ assert.Equal(t, fmt.Sprintf("my-app-staging-%s", strings.ToLower(string(app.ID()))), conf.ProjectName())
+ })
+}
diff --git a/internal/deployment/domain/credentials_test.go b/internal/deployment/domain/credentials_test.go
index f273661a..5a55f43b 100644
--- a/internal/deployment/domain/credentials_test.go
+++ b/internal/deployment/domain/credentials_test.go
@@ -4,15 +4,15 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_Credentials(t *testing.T) {
t.Run("should be instantiable", func(t *testing.T) {
cred := domain.NewCredentials("user", "pass")
- testutil.Equals(t, "user", cred.Username())
- testutil.Equals(t, "pass", cred.Password())
+ assert.Equal(t, "user", cred.Username())
+ assert.Equal(t, "pass", cred.Password())
})
t.Run("should be able to change the username", func(t *testing.T) {
@@ -20,8 +20,8 @@ func Test_Credentials(t *testing.T) {
cred.HasUsername("newuser")
- testutil.Equals(t, "newuser", cred.Username())
- testutil.Equals(t, "pass", cred.Password())
+ assert.Equal(t, "newuser", cred.Username())
+ assert.Equal(t, "pass", cred.Password())
})
t.Run("should be able to change the password", func(t *testing.T) {
@@ -29,7 +29,7 @@ func Test_Credentials(t *testing.T) {
cred.HasPassword("newpass")
- testutil.Equals(t, "user", cred.Username())
- testutil.Equals(t, "newpass", cred.Password())
+ assert.Equal(t, "user", cred.Username())
+ assert.Equal(t, "newpass", cred.Password())
})
}
diff --git a/internal/deployment/domain/deployment.go b/internal/deployment/domain/deployment.go
index d004ea73..9dbdcf28 100644
--- a/internal/deployment/domain/deployment.go
+++ b/internal/deployment/domain/deployment.go
@@ -17,6 +17,15 @@ var (
ErrCouldNotPromoteProductionDeployment = apperr.New("could_not_promote_production_deployment")
ErrRunningOrPendingDeployments = apperr.New("running_or_pending_deployments")
ErrInvalidSourceDeployment = apperr.New("invalid_source_deployment")
+ ErrNotInPendingState = apperr.New("not_in_pending_state")
+ ErrNotInRunningState = apperr.New("not_in_running_state")
+)
+
+const (
+ DeploymentStatusPending DeploymentStatus = iota
+ DeploymentStatusRunning
+ DeploymentStatusFailed
+ DeploymentStatusSucceeded
)
type (
@@ -28,7 +37,7 @@ type (
event.Emitter
id DeploymentID
- config DeploymentConfig
+ config ConfigSnapshot
state DeploymentState
source SourceData
requested shared.Action[domain.UserID]
@@ -44,7 +53,7 @@ type (
HasDeploymentsOnAppTargetEnv(context.Context, AppID, TargetID, Environment, shared.TimeInterval) (HasRunningOrPendingDeploymentsOnAppTargetEnv, HasSuccessfulDeploymentsOnAppTargetEnv, error)
}
- FailCriterias struct {
+ FailCriteria struct {
Status monad.Maybe[DeploymentStatus]
Target monad.Maybe[TargetID]
App monad.Maybe[AppID]
@@ -52,7 +61,7 @@ type (
}
DeploymentsWriter interface {
- FailDeployments(context.Context, error, FailCriterias) error // Fail all deployments matching the given filters
+ FailDeployments(context.Context, error, FailCriteria) error // Fail all deployments matching the given filters
Write(context.Context, ...*Deployment) error
}
@@ -60,7 +69,7 @@ type (
bus.Notification
ID DeploymentID
- Config DeploymentConfig
+ Config ConfigSnapshot
State DeploymentState
Source SourceData
Requested shared.Action[domain.UserID]
@@ -70,7 +79,7 @@ type (
bus.Notification
ID DeploymentID
- Config DeploymentConfig
+ Config ConfigSnapshot
State DeploymentState
}
)
@@ -98,7 +107,7 @@ func (a *App) NewDeployment(
return d, ErrVersionControlNotConfigured
}
- conf, err := a.ConfigSnapshotFor(env)
+ conf, err := a.configSnapshotFor(env)
if err != nil {
return d, err
@@ -182,15 +191,13 @@ func (a *App) Promote(
}
func (d *Deployment) ID() DeploymentID { return d.id }
-func (d *Deployment) Config() DeploymentConfig { return d.config }
+func (d *Deployment) Config() ConfigSnapshot { return d.config }
func (d *Deployment) Source() SourceData { return d.source }
func (d *Deployment) Requested() shared.Action[domain.UserID] { return d.requested }
// Mark a deployment has started.
func (d *Deployment) HasStarted() error {
- err := d.state.Started()
-
- if err != nil {
+ if err := d.state.started(); err != nil {
return err
}
@@ -199,23 +206,10 @@ func (d *Deployment) HasStarted() error {
return nil
}
-// Mark the deployment has ended with availables services or with an error if any.
+// Mark the deployment has ended with available services or with an error if any.
// The internal status of the deployment will be updated accordingly.
func (d *Deployment) HasEnded(services Services, deploymentErr error) error {
- // No services and no errors, that strange but assume a deployment without services.
- if services == nil && deploymentErr == nil {
- services = Services{}
- }
-
- var err error
-
- if deploymentErr != nil {
- err = d.state.Failed(deploymentErr)
- } else {
- err = d.state.Succeeded(services)
- }
-
- if err != nil {
+ if err := d.state.ended(services, deploymentErr); err != nil {
return err
}
@@ -246,3 +240,59 @@ func (d *Deployment) apply(e event.Event) {
event.Store(d, e)
}
+
+type (
+ DeploymentStatus uint8
+
+ // Holds together information related to the current deployment state. With a value
+ // object, it is easier to validate consistency between all those related properties.
+ // The default value represents a pending state.
+ DeploymentState struct {
+ status DeploymentStatus
+ errcode monad.Maybe[string]
+ services monad.Maybe[Services]
+ startedAt monad.Maybe[time.Time]
+ finishedAt monad.Maybe[time.Time]
+ }
+)
+
+func (s *DeploymentState) started() error {
+ if s.status != DeploymentStatusPending {
+ return ErrNotInPendingState
+ }
+
+ s.status = DeploymentStatusRunning
+ s.startedAt.Set(time.Now().UTC())
+
+ return nil
+}
+
+func (s *DeploymentState) ended(services Services, err error) error {
+ if s.status != DeploymentStatusRunning {
+ return ErrNotInRunningState
+ }
+
+ s.finishedAt.Set(time.Now().UTC())
+
+ if err != nil {
+ s.status = DeploymentStatusFailed
+ s.errcode.Set(err.Error())
+ return nil
+ }
+
+ s.status = DeploymentStatusSucceeded
+
+ if services == nil {
+ services = make(Services, 0)
+ }
+
+ s.services.Set(services)
+
+ return nil
+}
+
+func (s DeploymentState) Status() DeploymentStatus { return s.status }
+func (s DeploymentState) ErrCode() monad.Maybe[string] { return s.errcode }
+func (s DeploymentState) Services() monad.Maybe[Services] { return s.services }
+func (s DeploymentState) StartedAt() monad.Maybe[time.Time] { return s.startedAt }
+func (s DeploymentState) FinishedAt() monad.Maybe[time.Time] { return s.finishedAt }
diff --git a/internal/deployment/domain/deployment_config_test.go b/internal/deployment/domain/deployment_config_test.go
deleted file mode 100644
index b2caad78..00000000
--- a/internal/deployment/domain/deployment_config_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package domain_test
-
-import (
- "fmt"
- "strings"
- "testing"
-
- "github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
-)
-
-func Test_Config(t *testing.T) {
- production := domain.NewEnvironmentConfig("production-target")
- production.HasEnvironmentVariables(domain.ServicesEnv{
- "app": {"DEBUG": "false"},
- "db": {"USERNAME": "prodadmin"},
- })
-
- staging := domain.NewEnvironmentConfig("staging-target")
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(production, true, true),
- domain.NewEnvironmentConfigRequirement(staging, true, true),
- "uid"))
- appidLower := strings.ToLower(string(app.ID()))
-
- t.Run("could be created from an app", func(t *testing.T) {
- conf, err := app.ConfigSnapshotFor(domain.Production)
-
- testutil.IsNil(t, err)
- testutil.Equals(t, "my-app", conf.AppName())
- testutil.Equals(t, domain.Production, conf.Environment())
- testutil.Equals(t, production.Target(), conf.Target())
- testutil.DeepEquals(t, production.Vars(), conf.Vars())
- })
-
- t.Run("should fail if env is not valid", func(t *testing.T) {
- _, err := app.ConfigSnapshotFor("invalid")
-
- testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, err)
- })
-
- t.Run("should provide a way to retrieve environment variables for a service name", func(t *testing.T) {
- conf, _ := app.ConfigSnapshotFor(domain.Production)
-
- testutil.IsFalse(t, conf.EnvironmentVariablesFor("otherservice").HasValue())
- testutil.IsTrue(t, conf.EnvironmentVariablesFor("app").HasValue())
- testutil.DeepEquals(t, domain.EnvVars{
- "DEBUG": "false",
- }, conf.EnvironmentVariablesFor("app").MustGet())
- })
-
- t.Run("should return an empty monad if no environment variables are defined at all", func(t *testing.T) {
- conf, _ := app.ConfigSnapshotFor(domain.Staging)
-
- testutil.IsFalse(t, conf.EnvironmentVariablesFor("app").HasValue())
- })
-
- t.Run("should generate a subdomain equals to app name if env is production", func(t *testing.T) {
- conf, _ := app.ConfigSnapshotFor(domain.Production)
-
- testutil.Equals(t, "my-app", conf.SubDomain("app", true))
- testutil.Equals(t, "db.my-app", conf.SubDomain("db", false))
- })
-
- t.Run("should generate a subdomain suffixed by the env if not production", func(t *testing.T) {
- conf, _ := app.ConfigSnapshotFor(domain.Staging)
-
- testutil.Equals(t, "my-app-staging", conf.SubDomain("app", true))
- testutil.Equals(t, "db.my-app-staging", conf.SubDomain("db", false))
- })
-
- t.Run("should expose a unique project name", func(t *testing.T) {
- conf, _ := app.ConfigSnapshotFor(domain.Staging)
-
- testutil.Equals(t, fmt.Sprintf("my-app-staging-%s", appidLower), conf.ProjectName())
- })
-
- t.Run("should expose a unique image name for a service", func(t *testing.T) {
- conf, _ := app.ConfigSnapshotFor(domain.Staging)
-
- testutil.Equals(t, fmt.Sprintf("my-app-%s/app:staging", appidLower), conf.ImageName("app"))
- })
-
- t.Run("should expose a unique qualified name for a service", func(t *testing.T) {
- conf, _ := app.ConfigSnapshotFor(domain.Staging)
-
- testutil.Equals(t, fmt.Sprintf("my-app-staging-%s-app", appidLower), conf.QualifiedName("app"))
- })
-}
diff --git a/internal/deployment/domain/deployment_id_test.go b/internal/deployment/domain/deployment_id_test.go
index 133e972a..8f16bdc7 100644
--- a/internal/deployment/domain/deployment_id_test.go
+++ b/internal/deployment/domain/deployment_id_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_DeploymentID(t *testing.T) {
@@ -16,7 +16,7 @@ func Test_DeploymentID(t *testing.T) {
id := domain.DeploymentIDFrom(app, number)
- testutil.Equals(t, app, id.AppID())
- testutil.Equals(t, number, id.DeploymentNumber())
+ assert.Equal(t, app, id.AppID())
+ assert.Equal(t, number, id.DeploymentNumber())
})
}
diff --git a/internal/deployment/domain/deployment_test.go b/internal/deployment/domain/deployment_test.go
index 5508fc96..14f5801f 100644
--- a/internal/deployment/domain/deployment_test.go
+++ b/internal/deployment/domain/deployment_test.go
@@ -6,220 +6,265 @@ import (
auth "github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
)
func Test_Deployment(t *testing.T) {
- var (
- appname domain.AppName = "my-app"
- production = domain.NewEnvironmentConfig("production-target")
- staging = domain.NewEnvironmentConfig("staging-target")
- productionAvailable = domain.NewEnvironmentConfigRequirement(production, true, true)
- stagingAvailable = domain.NewEnvironmentConfigRequirement(staging, true, true)
- uid auth.UserID = "uid"
- number domain.DeploymentNumber = 1
- vcsMeta = meta{true}
- nonVcsMeta = meta{false}
- app = must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
- )
t.Run("should require a version control config to be defined on the app for vcs managed source", func(t *testing.T) {
- _, err := app.NewDeployment(number, vcsMeta, domain.Production, uid)
+ app := fixture.App()
+ _, err := app.NewDeployment(1, fixture.SourceData(fixture.WithVersionControlNeeded()), domain.Production, "uid")
- testutil.ErrorIs(t, domain.ErrVersionControlNotConfigured, err)
+ assert.ErrorIs(t, domain.ErrVersionControlNotConfigured, err)
})
t.Run("should require an app without cleanup requested", func(t *testing.T) {
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
- app.RequestCleanup(uid)
+ app := fixture.App()
+ app.RequestCleanup("uid")
- _, err := app.NewDeployment(number, nonVcsMeta, domain.Production, uid)
+ _, err := app.NewDeployment(1, fixture.SourceData(), domain.Production, "uid")
- testutil.ErrorIs(t, domain.ErrAppCleanupRequested, err)
+ assert.ErrorIs(t, domain.ErrAppCleanupRequested, err)
})
t.Run("should fail for an invalid environment", func(t *testing.T) {
- _, err := app.NewDeployment(number, nonVcsMeta, "doesnotexist", uid)
+ app := fixture.App()
+ _, err := app.NewDeployment(1, fixture.SourceData(), "doesnotexist", "uid")
- testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, err)
+ assert.ErrorIs(t, domain.ErrInvalidEnvironmentName, err)
})
t.Run("should be created from a valid app", func(t *testing.T) {
- dpl, err := app.NewDeployment(number, nonVcsMeta, domain.Production, uid)
+ config := domain.NewEnvironmentConfig("production-target")
+ app := fixture.App(fixture.WithAppName("my-app"), fixture.WithProductionConfig(config))
+ sourceData := fixture.SourceData()
+ dpl, err := app.NewDeployment(1, sourceData, domain.Production, "uid")
conf := dpl.Config()
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.DeploymentIDFrom(app.ID(), number), dpl.ID())
- testutil.Equals(t, nonVcsMeta, dpl.Source().(meta))
- testutil.Equals(t, app.ID(), conf.AppID())
- testutil.Equals(t, "my-app", conf.AppName())
- testutil.Equals(t, domain.Production, conf.Environment())
- testutil.Equals(t, production.Target(), conf.Target())
- testutil.DeepEquals(t, production.Vars(), conf.Vars())
-
- testutil.HasNEvents(t, &dpl, 1)
- evt := testutil.EventIs[domain.DeploymentCreated](t, &dpl, 0)
-
- testutil.Equals(t, dpl.ID(), evt.ID)
- testutil.Equals(t, dpl.Source(), evt.Source)
- testutil.Equals(t, domain.DeploymentStatusPending, evt.State.Status())
- testutil.IsFalse(t, evt.Requested.At().IsZero())
- testutil.Equals(t, uid, evt.Requested.By())
+ assert.Nil(t, err)
+ assert.Equal(t, domain.DeploymentIDFrom(app.ID(), 1), dpl.ID())
+ assert.NotZero(t, dpl.Requested())
+ assert.Equal(t, sourceData, dpl.Source())
+ assert.Equal(t, app.ID(), conf.AppID())
+ assert.Equal(t, "my-app", conf.AppName())
+ assert.Equal(t, domain.Production, conf.Environment())
+ assert.Equal(t, config.Target(), conf.Target())
+ assert.DeepEqual(t, config.Vars(), conf.Vars())
+
+ assert.HasNEvents(t, 1, &dpl)
+ evt := assert.EventIs[domain.DeploymentCreated](t, &dpl, 0)
+
+ assert.DeepEqual(t, domain.DeploymentCreated{
+ ID: dpl.ID(),
+ Config: dpl.Config(),
+ State: evt.State,
+ Source: dpl.Source(),
+ Requested: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, evt.Requested.At())),
+ }, evt)
+
+ assert.Equal(t, domain.DeploymentStatusPending, evt.State.Status())
+ assert.Zero(t, evt.State.ErrCode())
+ assert.False(t, evt.State.Services().HasValue())
})
t.Run("could be marked has started", func(t *testing.T) {
- var err error
+ t.Run("should fail if the deployment is already started", func(t *testing.T) {
+ deployment := fixture.Deployment()
+ assert.Nil(t, deployment.HasStarted())
- dpl, err := app.NewDeployment(number, nonVcsMeta, domain.Production, uid)
+ err := deployment.HasStarted()
- testutil.IsNil(t, err)
+ assert.ErrorIs(t, domain.ErrNotInPendingState, err)
+ })
- err = dpl.HasStarted()
+ t.Run("should succeed if the deployment is not started yet", func(t *testing.T) {
+ deployment := fixture.Deployment()
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &dpl, 2)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 1)
+ err := deployment.HasStarted()
- testutil.Equals(t, dpl.ID(), evt.ID)
- testutil.Equals(t, domain.DeploymentStatusRunning, evt.State.Status())
+ assert.Nil(t, err)
+ assert.HasNEvents(t, 2, &deployment)
+ evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 1)
+
+ assert.Equal(t, deployment.ID(), evt.ID)
+ assert.Equal(t, domain.DeploymentStatusRunning, evt.State.Status())
+ assert.NotZero(t, evt.State.StartedAt())
+ assert.Zero(t, evt.State.ErrCode())
+ assert.False(t, evt.State.Services().HasValue())
+ assert.Zero(t, evt.State.FinishedAt())
+ })
})
- t.Run("could be marked has ended with services", func(t *testing.T) {
- var err error
+ t.Run("could be marked has ended", func(t *testing.T) {
+ t.Run("should fail if the deployment is not in running state", func(t *testing.T) {
+ deployment := fixture.Deployment()
- dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Production, uid))
- services := domain.Services{
- dpl.Config().NewService("aservice", "an/image"),
- }
+ err := deployment.HasEnded(nil, nil)
- dpl.HasStarted()
+ assert.ErrorIs(t, domain.ErrNotInRunningState, err)
+ })
- err = dpl.HasEnded(services, nil)
+ t.Run("should succeed if services is set and no error happened", func(t *testing.T) {
+ deployment := fixture.Deployment()
+ builder := deployment.Config().ServicesBuilder()
+ builder.AddService("service", "an/image")
+ services := builder.Services()
+ assert.Nil(t, deployment.HasStarted())
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &dpl, 3)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 1)
- testutil.Equals(t, domain.DeploymentStatusRunning, evt.State.Status())
+ err := deployment.HasEnded(services, nil)
- evt = testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2)
+ assert.Nil(t, err)
+ assert.HasNEvents(t, 3, &deployment, "should have events related to deployment started and ended")
- testutil.Equals(t, dpl.ID(), evt.ID)
- testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status())
- testutil.DeepEquals(t, services, evt.State.Services().MustGet())
- })
+ evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 2)
- t.Run("should default to a deployment without services if has ended without services nor error", func(t *testing.T) {
- var err error
+ assert.Equal(t, deployment.ID(), evt.ID)
+ assert.Equal(t, domain.DeploymentStatusSucceeded, evt.State.Status())
+ assert.DeepEqual(t, services, evt.State.Services().MustGet())
+ assert.NotZero(t, evt.State.StartedAt())
+ assert.NotZero(t, evt.State.FinishedAt())
+ assert.Zero(t, evt.State.ErrCode())
+ })
- dpl, _ := app.NewDeployment(number, nonVcsMeta, domain.Production, uid)
- dpl.HasStarted()
+ t.Run("should default to an empty services array if nil given and no error", func(t *testing.T) {
+ deployment := fixture.Deployment()
+ assert.Nil(t, deployment.HasStarted())
- err = dpl.HasEnded(nil, nil)
+ err := deployment.HasEnded(nil, nil)
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &dpl, 3)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2)
+ assert.Nil(t, err)
+ assert.HasNEvents(t, 3, &deployment, "should have events related to deployment started and ended")
- testutil.Equals(t, dpl.ID(), evt.ID)
- testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status())
- testutil.IsTrue(t, evt.State.Services().HasValue())
- })
+ evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 2)
+
+ assert.Equal(t, deployment.ID(), evt.ID)
+ assert.Equal(t, domain.DeploymentStatusSucceeded, evt.State.Status())
+ assert.DeepEqual(t, domain.Services{}, evt.State.Services().MustGet())
+ assert.NotZero(t, evt.State.StartedAt())
+ assert.NotZero(t, evt.State.FinishedAt())
+ assert.Zero(t, evt.State.ErrCode())
+ })
- t.Run("could be marked has ended with an error", func(t *testing.T) {
- var (
- err error
- reason = errors.New("failed reason")
- )
+ t.Run("should be marked has failed if an error is given", func(t *testing.T) {
+ reason := errors.New("failed reason")
+ deployment := fixture.Deployment()
+ assert.Nil(t, deployment.HasStarted())
- dpl, _ := app.NewDeployment(number, nonVcsMeta, domain.Production, uid)
- dpl.HasStarted()
+ err := deployment.HasEnded(nil, reason)
- err = dpl.HasEnded(nil, reason)
+ assert.Nil(t, err)
+ assert.HasNEvents(t, 3, &deployment, "should have events related to deployment started and ended")
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &dpl, 3)
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2)
+ evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 2)
- testutil.Equals(t, dpl.ID(), evt.ID)
- testutil.Equals(t, domain.DeploymentStatusFailed, evt.State.Status())
- testutil.Equals(t, reason.Error(), evt.State.ErrCode().MustGet())
- testutil.IsFalse(t, evt.State.Services().HasValue())
+ assert.Equal(t, deployment.ID(), evt.ID)
+ assert.Equal(t, domain.DeploymentStatusFailed, evt.State.Status())
+ assert.Equal(t, reason.Error(), evt.State.ErrCode().MustGet())
+ assert.NotZero(t, evt.State.FinishedAt())
+ assert.False(t, evt.State.Services().HasValue())
+ })
})
t.Run("could be redeployed", func(t *testing.T) {
- dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Production, uid))
+ app := fixture.App()
+ sourceDeployment := fixture.Deployment(fixture.FromApp(app))
+
+ newDeployment, err := app.Redeploy(sourceDeployment, 2, "another-user")
+
+ assert.Nil(t, err)
+ assert.Equal(t, domain.DeploymentIDFrom(app.ID(), sourceDeployment.ID().DeploymentNumber()+1), newDeployment.ID())
+ assert.DeepEqual(t, sourceDeployment.Config(), newDeployment.Config())
+ assert.Equal(t, sourceDeployment.Source(), newDeployment.Source())
+ assert.NotZero(t, newDeployment.Requested())
+
+ evt := assert.EventIs[domain.DeploymentCreated](t, &newDeployment, 0)
+
+ assert.DeepEqual(t, domain.DeploymentCreated{
+ ID: newDeployment.ID(),
+ Config: sourceDeployment.Config(),
+ State: evt.State,
+ Source: sourceDeployment.Source(),
+ Requested: shared.ActionFrom[auth.UserID]("another-user", assert.NotZero(t, evt.Requested.At())),
+ }, evt)
- redpl, err := app.Redeploy(dpl, 2, "another-user")
+ assert.Equal(t, domain.DeploymentStatusPending, evt.State.Status())
+ assert.Zero(t, evt.State.ErrCode())
+ assert.False(t, evt.State.Services().HasValue())
- testutil.IsNil(t, err)
- testutil.Equals(t, dpl.Config().Environment(), redpl.Config().Environment())
- testutil.Equals(t, dpl.Source(), redpl.Source())
})
t.Run("should err if trying to redeploy a deployment on the wrong app", func(t *testing.T) {
- source := must.Panic(app.NewDeployment(1, nonVcsMeta, domain.Production, uid))
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ source := fixture.Deployment()
+ anotherApp := fixture.App()
- _, err := app.Redeploy(source, 2, "uid")
+ _, err := anotherApp.Redeploy(source, 2, "uid")
- testutil.ErrorIs(t, domain.ErrInvalidSourceDeployment, err)
+ assert.ErrorIs(t, domain.ErrInvalidSourceDeployment, err)
})
t.Run("could not promote an already in production deployment", func(t *testing.T) {
- dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Production, uid))
+ app := fixture.App()
+ dpl := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Production))
_, err := app.Promote(dpl, 2, "another-user")
- testutil.ErrorIs(t, domain.ErrCouldNotPromoteProductionDeployment, err)
+ assert.ErrorIs(t, domain.ErrCouldNotPromoteProductionDeployment, err)
})
t.Run("should err if trying to promote a deployment on the wrong app", func(t *testing.T) {
- source := must.Panic(app.NewDeployment(1, nonVcsMeta, domain.Staging, uid))
- app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid))
+ source := fixture.Deployment(fixture.ForEnvironment(domain.Staging))
+ anotherApp := fixture.App()
- _, err := app.Promote(source, 2, "uid")
+ _, err := anotherApp.Promote(source, 2, "uid")
- testutil.ErrorIs(t, domain.ErrInvalidSourceDeployment, err)
+ assert.ErrorIs(t, domain.ErrInvalidSourceDeployment, err)
})
t.Run("could promote a staging deployment", func(t *testing.T) {
- dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Staging, uid))
-
- promoted, err := app.Promote(dpl, 2, "another-user")
-
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.Production, promoted.Config().Environment())
- testutil.Equals(t, dpl.Source(), promoted.Source())
+ productionConfig := domain.NewEnvironmentConfig("production-target")
+ app := fixture.App(fixture.WithProductionConfig(productionConfig))
+ sourceDeployment := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Staging))
+
+ promoted, err := app.Promote(sourceDeployment, 2, "another-user")
+
+ assert.Nil(t, err)
+ assert.Equal(t, domain.DeploymentIDFrom(app.ID(), 2), promoted.ID())
+ assert.Equal(t, sourceDeployment.Config().AppID(), promoted.Config().AppID())
+ assert.Equal(t, sourceDeployment.Config().AppName(), promoted.Config().AppName())
+ assert.Equal(t, productionConfig.Target(), promoted.Config().Target())
+ assert.Equal(t, domain.Production, promoted.Config().Environment())
+ assert.DeepEqual(t, sourceDeployment.Config().Vars(), promoted.Config().Vars())
+ assert.Equal(t, sourceDeployment.Source(), promoted.Source())
+ assert.NotZero(t, promoted.Requested())
+
+ evt := assert.EventIs[domain.DeploymentCreated](t, &promoted, 0)
+
+ assert.DeepEqual(t, domain.DeploymentCreated{
+ ID: promoted.ID(),
+ Config: promoted.Config(),
+ State: evt.State,
+ Source: sourceDeployment.Source(),
+ Requested: shared.ActionFrom[auth.UserID]("another-user", assert.NotZero(t, evt.Requested.At())),
+ }, evt)
})
}
func Test_DeploymentEvents(t *testing.T) {
t.Run("DeploymentStateChanged should expose a method to check for success state", func(t *testing.T) {
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true),
- "uid",
- ))
- dpl := must.Panic(app.NewDeployment(1, meta{}, domain.Staging, "uid"))
- testutil.IsNil(t, dpl.HasStarted())
- testutil.IsNil(t, dpl.HasEnded(nil, nil))
-
- evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2)
- testutil.IsTrue(t, evt.HasSucceeded())
-
- dpl = must.Panic(app.NewDeployment(2, meta{}, domain.Staging, "uid"))
- testutil.IsNil(t, dpl.HasStarted())
- testutil.IsNil(t, dpl.HasEnded(nil, errors.New("failed")))
-
- evt = testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2)
- testutil.IsFalse(t, evt.HasSucceeded())
- })
-}
+ dpl := fixture.Deployment()
+ assert.Nil(t, dpl.HasStarted())
+ assert.Nil(t, dpl.HasEnded(nil, nil))
-type meta struct {
- isVCS bool
-}
+ evt := assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 2)
+ assert.True(t, evt.HasSucceeded())
+
+ dpl = fixture.Deployment()
+ assert.Nil(t, dpl.HasStarted())
+ assert.Nil(t, dpl.HasEnded(nil, errors.New("failed")))
-func (meta) Kind() string { return "test" }
-func (m meta) NeedVersionControl() bool { return m.isVCS }
+ evt = assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 2)
+ assert.False(t, evt.HasSucceeded())
+ })
+}
diff --git a/internal/deployment/domain/environment.go b/internal/deployment/domain/environment.go
index e3c0d723..88d1380d 100644
--- a/internal/deployment/domain/environment.go
+++ b/internal/deployment/domain/environment.go
@@ -73,6 +73,14 @@ func (e EnvironmentConfig) Target() TargetID { return e.target }
func (e EnvironmentConfig) Version() time.Time { return e.version }
func (e EnvironmentConfig) Vars() monad.Maybe[ServicesEnv] { return e.vars }
+func (e *EnvironmentConfig) consolidate(other EnvironmentConfig) {
+ if e.target != other.target {
+ return
+ }
+
+ e.version = other.version
+}
+
// Builds the map of services variables from a raw value.
func ServicesEnvFrom(raw map[string]map[string]string) ServicesEnv {
result := make(ServicesEnv, len(raw))
diff --git a/internal/deployment/domain/environment_test.go b/internal/deployment/domain/environment_test.go
index 3ac5bfd7..95a728e5 100644
--- a/internal/deployment/domain/environment_test.go
+++ b/internal/deployment/domain/environment_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_Environment(t *testing.T) {
@@ -25,11 +25,11 @@ func Test_Environment(t *testing.T) {
r, err := domain.EnvironmentFrom(test.input)
if test.valid {
- testutil.Equals(t, domain.Environment(test.input), r)
- testutil.IsNil(t, err)
+ assert.Equal(t, domain.Environment(test.input), r)
+ assert.Nil(t, err)
} else {
- testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, err)
- testutil.Equals(t, "", r)
+ assert.ErrorIs(t, domain.ErrInvalidEnvironmentName, err)
+ assert.Equal(t, "", r)
}
})
}
@@ -46,7 +46,7 @@ func Test_Environment(t *testing.T) {
for _, test := range tests {
t.Run(string(test.input), func(t *testing.T) {
- testutil.Equals(t, test.production, test.input.IsProduction())
+ assert.Equal(t, test.production, test.input.IsProduction())
})
}
})
@@ -61,7 +61,7 @@ func Test_ServicesEnv(t *testing.T) {
r := domain.ServicesEnvFrom(rawEnvs)
- testutil.DeepEquals(t, domain.ServicesEnv{
+ assert.DeepEqual(t, domain.ServicesEnv{
"app": {"DEBUG": "false"},
"db": {"USERNAME": "admin"},
}, r)
@@ -70,7 +70,7 @@ func Test_ServicesEnv(t *testing.T) {
t.Run("should returns an empty map if the raw one is nil", func(t *testing.T) {
r := domain.ServicesEnvFrom(nil)
- testutil.DeepEquals(t, domain.ServicesEnv{}, r)
+ assert.DeepEqual(t, domain.ServicesEnv{}, r)
})
t.Run("should skip nil environment variables values", func(t *testing.T) {
@@ -81,7 +81,7 @@ func Test_ServicesEnv(t *testing.T) {
r := domain.ServicesEnvFrom(rawEnvs)
- testutil.DeepEquals(t, domain.ServicesEnv{
+ assert.DeepEqual(t, domain.ServicesEnv{
"app": {"DEBUG": "false"},
}, r)
})
@@ -92,9 +92,9 @@ func Test_ServicesEnv(t *testing.T) {
"db": {"USERNAME": "admin"},
}.Value()
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
- testutil.Equals(t, `{"app":{"DEBUG":"false"},"db":{"USERNAME":"admin"}}`, str)
+ assert.Equal(t, `{"app":{"DEBUG":"false"},"db":{"USERNAME":"admin"}}`, str)
})
t.Run("should implement the Scanner interface", func(t *testing.T) {
@@ -102,8 +102,8 @@ func Test_ServicesEnv(t *testing.T) {
err := r.Scan(`{"app":{"DEBUG":"false"},"db":{"USERNAME":"admin"}}`)
- testutil.IsNil(t, err)
- testutil.DeepEquals(t, domain.ServicesEnv{
+ assert.Nil(t, err)
+ assert.DeepEqual(t, domain.ServicesEnv{
"app": {"DEBUG": "false"},
"db": {"USERNAME": "admin"},
}, r)
@@ -116,8 +116,8 @@ func Test_EnvironmentConfig(t *testing.T) {
r := domain.NewEnvironmentConfig(target)
- testutil.Equals(t, target, r.Target())
- testutil.IsFalse(t, r.Vars().HasValue())
+ assert.Equal(t, target, r.Target())
+ assert.False(t, r.Vars().HasValue())
})
t.Run("should be able to configure environment variables", func(t *testing.T) {
@@ -130,9 +130,9 @@ func Test_EnvironmentConfig(t *testing.T) {
r := domain.NewEnvironmentConfig(target)
r.HasEnvironmentVariables(vars)
- testutil.Equals(t, target, r.Target())
- testutil.IsTrue(t, r.Vars().HasValue())
- testutil.DeepEquals(t, vars, r.Vars().MustGet())
+ assert.Equal(t, target, r.Target())
+ assert.True(t, r.Vars().HasValue())
+ assert.DeepEqual(t, vars, r.Vars().MustGet())
})
t.Run("should be able to compare itself with another config", func(t *testing.T) {
@@ -193,10 +193,10 @@ func Test_EnvironmentConfig(t *testing.T) {
b := test.b()
t.Run(fmt.Sprintf("%v %v", a, b), func(t *testing.T) {
r := a.Equals(b)
- testutil.Equals(t, test.expected, r)
+ assert.Equal(t, test.expected, r)
r = b.Equals(a)
- testutil.Equals(t, test.expected, r)
+ assert.Equal(t, test.expected, r)
})
}
})
diff --git a/internal/deployment/domain/registry_test.go b/internal/deployment/domain/registry_test.go
index 81d83fba..f1d62947 100644
--- a/internal/deployment/domain/registry_test.go
+++ b/internal/deployment/domain/registry_test.go
@@ -3,94 +3,127 @@ package domain_test
import (
"testing"
+ auth "github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Registry(t *testing.T) {
t.Run("should returns an error if the url is not unique", func(t *testing.T) {
_, err := domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), false), "uid")
- testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, err)
+ assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, err)
})
t.Run("could be created from a valid url", func(t *testing.T) {
- r, err := domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")
-
- testutil.IsNil(t, err)
- created := testutil.EventIs[domain.RegistryCreated](t, &r, 0)
- testutil.Equals(t, "http://example.com", created.Url.String())
- testutil.NotEquals(t, "", created.ID)
- testutil.Equals(t, "uid", created.Created.By())
- testutil.IsFalse(t, created.Created.At().IsZero())
+ var (
+ url = must.Panic(domain.UrlFrom("http://example.com"))
+ name = "registry"
+ uid auth.UserID = "uid"
+ )
+
+ r, err := domain.NewRegistry(name, domain.NewRegistryUrlRequirement(url, true), uid)
+
+ assert.Nil(t, err)
+ assert.NotZero(t, r.ID())
+ assert.Equal(t, url, r.Url())
+ assert.Equal(t, name, r.Name())
+
+ created := assert.EventIs[domain.RegistryCreated](t, &r, 0)
+
+ assert.Equal(t, domain.RegistryCreated{
+ ID: r.ID(),
+ Name: name,
+ Url: url,
+ Created: shared.ActionFrom(uid, assert.NotZero(t, created.Created.At())),
+ }, created)
})
t.Run("could be renamed and raise the event only if different", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
+ r := fixture.Registry(fixture.WithRegistryName("registry"))
r.Rename("new registry")
r.Rename("new registry")
- testutil.HasNEvents(t, &r, 2)
+ assert.HasNEvents(t, 2, &r, "should raise the event once per different name")
- renamed := testutil.EventIs[domain.RegistryRenamed](t, &r, 1)
- testutil.Equals(t, r.ID(), renamed.ID)
- testutil.Equals(t, "new registry", renamed.Name)
+ renamed := assert.EventIs[domain.RegistryRenamed](t, &r, 1)
+
+ assert.Equal(t, domain.RegistryRenamed{
+ ID: r.ID(),
+ Name: "new registry",
+ }, renamed)
})
t.Run("should require a valid url when updating it", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
+ r := fixture.Registry()
err := r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://localhost:5000")), false))
- testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, err)
+ assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, err)
})
t.Run("could have its url changed and raise the event only if different", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
+ r := fixture.Registry(fixture.WithUrl(must.Panic(domain.UrlFrom("http://example.com"))))
+
+ assert.Nil(t, r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
- r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true))
- r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://localhost:5000")), true))
+ differentUrl := must.Panic(domain.UrlFrom("http://localhost:5000"))
+ assert.Nil(t, r.HasUrl(domain.NewRegistryUrlRequirement(differentUrl, true)))
- testutil.HasNEvents(t, &r, 2)
+ assert.HasNEvents(t, 2, &r, "should raise the event only if given url is different")
- changed := testutil.EventIs[domain.RegistryUrlChanged](t, &r, 1)
- testutil.Equals(t, r.ID(), changed.ID)
- testutil.Equals(t, "http://localhost:5000", changed.Url.String())
+ changed := assert.EventIs[domain.RegistryUrlChanged](t, &r, 1)
+
+ assert.Equal(t, domain.RegistryUrlChanged{
+ ID: r.ID(),
+ Url: differentUrl,
+ }, changed)
})
t.Run("could have credentials attached and raise the event only if different", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
+ r := fixture.Registry()
+ credentials := domain.NewCredentials("user", "password")
- r.UseAuthentication(domain.NewCredentials("user", "password"))
- r.UseAuthentication(domain.NewCredentials("user", "password"))
+ r.UseAuthentication(credentials)
+ r.UseAuthentication(credentials)
+
+ assert.HasNEvents(t, 2, &r, "should raise the event once per different credentials")
- testutil.HasNEvents(t, &r, 2)
+ changed := assert.EventIs[domain.RegistryCredentialsChanged](t, &r, 1)
- changed := testutil.EventIs[domain.RegistryCredentialsChanged](t, &r, 1)
- testutil.Equals(t, r.ID(), changed.ID)
- testutil.Equals(t, "user", changed.Credentials.Username())
- testutil.Equals(t, "password", changed.Credentials.Password())
+ assert.Equal(t, domain.RegistryCredentialsChanged{
+ ID: r.ID(),
+ Credentials: credentials,
+ }, changed)
})
t.Run("could have credentials removed and raise the event once", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
+ r := fixture.Registry()
r.UseAuthentication(domain.NewCredentials("user", "password"))
r.RemoveAuthentication()
r.RemoveAuthentication()
- removed := testutil.EventIs[domain.RegistryCredentialsRemoved](t, &r, 2)
- testutil.Equals(t, r.ID(), removed.ID)
+ removed := assert.EventIs[domain.RegistryCredentialsRemoved](t, &r, 2)
+
+ assert.Equal(t, domain.RegistryCredentialsRemoved{
+ ID: r.ID(),
+ }, removed)
})
t.Run("could be deleted", func(t *testing.T) {
- r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid"))
+ r := fixture.Registry()
r.Delete()
- deleted := testutil.EventIs[domain.RegistryDeleted](t, &r, 1)
- testutil.Equals(t, r.ID(), deleted.ID)
+ deleted := assert.EventIs[domain.RegistryDeleted](t, &r, 1)
+
+ assert.Equal(t, domain.RegistryDeleted{
+ ID: r.ID(),
+ }, deleted)
})
}
diff --git a/internal/deployment/domain/service.go b/internal/deployment/domain/service.go
index 1cfa3c72..d87229b2 100644
--- a/internal/deployment/domain/service.go
+++ b/internal/deployment/domain/service.go
@@ -33,105 +33,131 @@ type (
port Port
}
- HttpEntrypointOptions struct {
- // True if this entrypoint should take the default subdomain for an application.
- UseDefaultSubdomain bool
- // True if this entrypoint is natively managed by the target and does not require specific port exposure.
- Managed bool
- }
-
// Custom types to hold Service array which implements the Scanner and Valuer
// interface to store it as a json string in the database (no need to create another table for it).
Services []Service
// Hold data related to services deployed upon a deployment success.
Service struct {
- name string
- qualifiedName string
- image string
- entrypoints []Entrypoint
+ name string
+ image string
+ entrypoints []Entrypoint
}
-)
-// Try to parse the given port from a raw string.
-func ParsePort(raw string) (Port, error) {
- v, err := strconv.ParseUint(raw, 10, 0)
+ // Main structure used to build a services array. Manipulated by actual providers.
+ ServicesBuilder struct {
+ config ConfigSnapshot
+ defaultSubdomainAvailable bool
+ services []*ServiceBuilder
+ }
- if err != nil {
- return 0, ErrInvalidPort
+ ServiceBuilder struct {
+ parent *ServicesBuilder
+ qualifiedName string
+ subdomain monad.Maybe[string]
+ Service
}
+)
- return Port(v), nil
+// Returns a new builder used to ease the process of building the services array.
+func (c ConfigSnapshot) ServicesBuilder() ServicesBuilder {
+ return ServicesBuilder{
+ config: c,
+ defaultSubdomainAvailable: true,
+ }
}
-func (p Port) String() string { return strconv.FormatUint(uint64(p), 10) }
-func (p Port) Uint32() uint32 { return uint32(p) }
-
-func newEntrypointName(suffix string, router Router, port Port) EntrypointName {
- return EntrypointName(suffix + "-" + port.String() + "-" + string(router))
-}
+func (b *ServicesBuilder) AddService(name, image string) *ServiceBuilder {
+ // Check if the service already exists
+ for _, service := range b.services {
+ if service.Service.name == name {
+ return service
+ }
+ }
-// Creates a new service. If the image is empty, a unique image name will be
-// generated.
-func (c DeploymentConfig) NewService(name, image string) (s Service) {
- s.name = name
- s.qualifiedName = c.QualifiedName(name)
+ builder := &ServiceBuilder{
+ parent: b,
+ qualifiedName: b.config.qualifiedName(name),
+ Service: Service{
+ name: name,
+ image: image,
+ },
+ }
- if image == "" {
- s.image = c.ImageName(name)
- } else {
- s.image = image
+ if builder.Service.image == "" {
+ builder.Service.image = b.config.imageName(name)
}
- return s
-}
+ b.services = append(b.services, builder)
-// Adds an HTTP entrypoint to the service.
-// HTTP entrypoints can be marked as automatically managed meaning they do not need a
-// specific configuration and are natively handled by the target.
-func (s *Service) AddHttpEntrypoint(conf DeploymentConfig, port Port, options HttpEntrypointOptions) Entrypoint {
- for _, entry := range s.entrypoints {
- // Already have an HTTP endpoint on this service, copy the subdomain and add it as a custom one.
- if entry.router == RouterHttp {
- return s.addEntrypoint(RouterHttp, !options.Managed, port, entry.subdomain.Get(""))
- }
- }
+ return builder
+}
- return s.addEntrypoint(RouterHttp, !options.Managed, port, conf.SubDomain(s.name, options.UseDefaultSubdomain))
+func (b *ServiceBuilder) AddHttpEntrypoint(port Port, custom bool) Entrypoint {
+ return b.addEntrypoint(RouterHttp, port, custom)
}
-// Adds a custom TCP entrypoint.
-func (s *Service) AddTCPEntrypoint(port Port) Entrypoint {
- return s.addEntrypoint(RouterTcp, true, port)
+func (b *ServiceBuilder) AddTCPEntrypoint(port Port, custom bool) Entrypoint {
+ return b.addEntrypoint(RouterTcp, port, custom)
}
-// Adds a custom UDP entrypoint.
-func (s *Service) AddUDPEntrypoint(port Port) Entrypoint {
- return s.addEntrypoint(RouterUdp, true, port)
+func (b *ServiceBuilder) AddUDPEntrypoint(port Port, custom bool) Entrypoint {
+ return b.addEntrypoint(RouterUdp, port, custom)
}
-func (s *Service) addEntrypoint(router Router, isCustom bool, port Port, subdomain ...string) (e Entrypoint) {
- // Check if the entrypoint already exists
- for _, entry := range s.entrypoints {
+func (b *ServiceBuilder) addEntrypoint(router Router, port Port, custom bool) Entrypoint {
+ // Check if the entrypoint already exists and returns early
+ for _, entry := range b.Service.entrypoints {
if entry.port == port && entry.router == router {
return entry
}
}
- e.name = newEntrypointName(s.qualifiedName, router, port)
- e.isCustom = isCustom
- e.router = router
- e.port = port
+ entrypoint := Entrypoint{
+ name: newEntrypointName(b.qualifiedName, router, port),
+ isCustom: custom,
+ router: router,
+ port: port,
+ }
+
+ if router == RouterHttp {
+ if !b.subdomain.HasValue() {
+ b.subdomain.Set(b.parent.config.subDomain(b.Service.name, b.parent.defaultSubdomainAvailable))
+ b.parent.defaultSubdomainAvailable = false
+ }
+
+ entrypoint.subdomain = b.subdomain
+ }
+
+ b.Service.entrypoints = append(b.Service.entrypoints, entrypoint)
- if len(subdomain) > 0 {
- e.subdomain.Set(subdomain[0])
+ return entrypoint
+}
+
+func (b *ServicesBuilder) Services() Services {
+ services := make(Services, len(b.services))
+
+ for i, service := range b.services {
+ services[i] = service.Service
}
- s.entrypoints = append(s.entrypoints, e)
+ return services
+}
+
+// Try to parse the given port from a raw string.
+func ParsePort(raw string) (Port, error) {
+ v, err := strconv.ParseUint(raw, 10, 0)
+
+ if err != nil {
+ return 0, ErrInvalidPort
+ }
- return e
+ return Port(v), nil
}
+func (p Port) String() string { return strconv.FormatUint(uint64(p), 10) }
+func (p Port) Uint32() uint32 { return uint32(p) }
+
func (s Service) Name() string { return s.name }
func (s Service) Image() string { return s.image }
@@ -155,7 +181,7 @@ func (e EntrypointName) Protocol() string {
return string(p)
}
-// Retrieve entrypoints for this service.
+// Retrieve all entrypoints for every services.
func (s Services) Entrypoints() []Entrypoint {
var result []Entrypoint
@@ -166,7 +192,7 @@ func (s Services) Entrypoints() []Entrypoint {
return result
}
-// Retrieve custom entrypoints for this service. Ones that are not natively
+// Retrieve all custom entrypoints. Ones that are not natively
// managed by the target and requires a manual configuration.
func (s Services) CustomEntrypoints() []Entrypoint {
return slices.DeleteFunc(s.Entrypoints(), isNotCustom)
@@ -179,6 +205,10 @@ func isNotCustom(entrypoint Entrypoint) bool {
return !entrypoint.isCustom
}
+func newEntrypointName(prefix string, router Router, port Port) EntrypointName {
+ return EntrypointName(prefix + "-" + port.String() + "-" + string(router))
+}
+
// Types needed to marshal an unexposed Service data.
type (
marshalledEntrypoint struct {
@@ -190,23 +220,21 @@ type (
}
marshalledService struct {
- Name string `json:"name"`
- QualifiedName string `json:"qualified_name"`
- Image string `json:"image"`
- Entrypoints []marshalledEntrypoint `json:"entrypoints"`
+ Name string `json:"name"`
+ Image string `json:"image"`
+ Entrypoints []marshalledEntrypoint `json:"entrypoints"`
}
)
func (s Service) MarshalJSON() ([]byte, error) {
- serv := marshalledService{
- Name: s.name,
- QualifiedName: s.qualifiedName,
- Image: s.image,
- Entrypoints: make([]marshalledEntrypoint, len(s.entrypoints)),
+ service := marshalledService{
+ Name: s.name,
+ Image: s.image,
+ Entrypoints: make([]marshalledEntrypoint, len(s.entrypoints)),
}
for i, entry := range s.entrypoints {
- serv.Entrypoints[i] = marshalledEntrypoint{
+ service.Entrypoints[i] = marshalledEntrypoint{
Name: string(entry.name),
IsCustom: entry.isCustom,
Router: entry.router,
@@ -215,7 +243,7 @@ func (s Service) MarshalJSON() ([]byte, error) {
}
}
- return json.Marshal(serv)
+ return json.Marshal(service)
}
func (s *Service) UnmarshalJSON(b []byte) error {
@@ -227,7 +255,6 @@ func (s *Service) UnmarshalJSON(b []byte) error {
s.image = m.Image
s.name = m.Name
- s.qualifiedName = m.QualifiedName
s.entrypoints = make([]Entrypoint, len(m.Entrypoints))
for i, entry := range m.Entrypoints {
diff --git a/internal/deployment/domain/service_test.go b/internal/deployment/domain/service_test.go
index fc751739..8fc0bf71 100644
--- a/internal/deployment/domain/service_test.go
+++ b/internal/deployment/domain/service_test.go
@@ -6,145 +6,191 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
-func Test_Service(t *testing.T) {
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true),
- "uid"))
- appidLower := strings.ToLower(string(app.ID()))
- config := must.Panic(app.ConfigSnapshotFor(domain.Production))
+func Test_ServicesBuilder(t *testing.T) {
t.Run("could be created from a deployment configuration", func(t *testing.T) {
- s := config.NewService("db", "postgres:14-alpine")
+ t.Run("should use the given image if any", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ deployment := fixture.Deployment(fixture.FromApp(app))
- testutil.Equals(t, "db", s.Name())
- testutil.Equals(t, "postgres:14-alpine", s.Image())
+ builder := deployment.Config().ServicesBuilder()
+ service := builder.AddService("db", "postgres:14-alpine")
- s = config.NewService("app", "")
+ assert.Equal(t, "db", service.Name())
+ assert.Equal(t, "postgres:14-alpine", service.Image())
+ })
- testutil.Equals(t, "app", s.Name())
- testutil.Equals(t, fmt.Sprintf("my-app-%s/app:production", appidLower), s.Image())
- })
+ t.Run("should generate a unique image name if not set", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ appidLower := strings.ToLower(string(app.ID()))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ builder := deployment.Config().ServicesBuilder()
- t.Run("should populate the subdomain when adding HTTP entrypoints", func(t *testing.T) {
- s := config.NewService("app", "")
+ service := builder.AddService("app", "")
- e := s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{
- Managed: true,
- UseDefaultSubdomain: true,
+ assert.Equal(t, "app", service.Name())
+ assert.Equal(t, fmt.Sprintf("my-app-%s/app:production", appidLower), service.Image())
})
- testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-80-http", appidLower), string(e.Name()))
- testutil.Equals(t, domain.RouterHttp, e.Router())
- testutil.IsFalse(t, e.IsCustom())
- testutil.Equals(t, "my-app", e.Subdomain().Get(""))
- testutil.Equals(t, 80, e.Port())
-
- e = s.AddHttpEntrypoint(config, 8080, domain.HttpEntrypointOptions{})
- testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-8080-http", appidLower), string(e.Name()))
- testutil.Equals(t, domain.RouterHttp, e.Router())
- testutil.IsTrue(t, e.IsCustom())
- testutil.Equals(t, "my-app", e.Subdomain().Get(""))
- testutil.Equals(t, 8080, e.Port())
-
- same := s.AddHttpEntrypoint(config, 8080, domain.HttpEntrypointOptions{})
- testutil.Equals(t, e, same)
})
- t.Run("could have one or more TCP/UDP entrypoints attached", func(t *testing.T) {
- s := config.NewService("app", "")
-
- tcp := s.AddTCPEntrypoint(8080)
- testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-8080-tcp", appidLower), string(tcp.Name()))
- testutil.Equals(t, domain.RouterTcp, tcp.Router())
- testutil.IsTrue(t, tcp.IsCustom())
- testutil.IsFalse(t, tcp.Subdomain().HasValue())
- testutil.Equals(t, 8080, tcp.Port())
-
- udp := s.AddUDPEntrypoint(8080)
- testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-8080-udp", appidLower), string(udp.Name()))
- testutil.Equals(t, domain.RouterUdp, udp.Router())
- testutil.IsTrue(t, udp.IsCustom())
- testutil.IsFalse(t, udp.Subdomain().HasValue())
- testutil.Equals(t, 8080, udp.Port())
-
- same := s.AddTCPEntrypoint(8080)
- testutil.Equals(t, tcp, same)
-
- same = s.AddUDPEntrypoint(8080)
- testutil.Equals(t, udp, same)
+ t.Run("should returns an existing service if trying to add one with the same name", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+
+ builder := deployment.Config().ServicesBuilder()
+
+ one := builder.AddService("app", "")
+ two := builder.AddService("app", "")
+
+ assert.HasLength(t, 1, builder.Services())
+ assert.Equal(t, one, two)
})
-}
-func Test_Services(t *testing.T) {
- app := must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true),
- "uid"))
- appidLower := strings.ToLower(string(app.ID()))
- config := must.Panic(app.ConfigSnapshotFor(domain.Production))
+ t.Run("should returns the existing entrypoint if trying to add one for the same router and port", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ deployment := fixture.Deployment(fixture.FromApp(app))
- t.Run("should be able to return all entrypoints", func(t *testing.T) {
- var services domain.Services
+ builder := deployment.Config().ServicesBuilder()
+ service := builder.AddService("app", "image")
+ entrypointOne := service.AddHttpEntrypoint(80, true)
+ entrypointTwo := service.AddHttpEntrypoint(80, false)
- s := config.NewService("app", "")
- http := s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{
- Managed: true,
- })
- udp := s.AddUDPEntrypoint(8080)
+ assert.HasLength(t, 1, builder.Services())
+ assert.Equal(t, 1, len(builder.Services().Entrypoints()))
+ assert.True(t, builder.Services().Entrypoints()[0].IsCustom())
+ assert.Equal(t, entrypointOne, entrypointTwo)
+ })
- services = append(services, s)
+ t.Run("could have http entrypoints added", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ appidLower := strings.ToLower(string(app.ID()))
+ deployment := fixture.Deployment(fixture.FromApp(app))
- s = config.NewService("db", "postgres:14-alpine")
- tcp := s.AddTCPEntrypoint(5432)
+ builder := deployment.Config().ServicesBuilder()
+ service := builder.AddService("app", "")
+ service.AddHttpEntrypoint(80, true)
+ service.AddHttpEntrypoint(8080, false)
+ service = builder.AddService("other", "")
+ service.AddHttpEntrypoint(3000, false)
- services = append(services, s)
+ services := builder.Services()
+ assert.HasLength(t, 2, services)
+ assert.Equal(t, "app", services[0].Name())
+ assert.Equal(t, fmt.Sprintf("my-app-%s/app:production", appidLower), services[0].Image())
- s = config.NewService("cache", "redis:6-alpine")
- services = append(services, s)
+ assert.Equal(t, "other", services[1].Name())
+ assert.Equal(t, fmt.Sprintf("my-app-%s/other:production", appidLower), services[1].Image())
entrypoints := services.Entrypoints()
+ assert.HasLength(t, 3, entrypoints)
+
+ assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-80-http", appidLower), string(entrypoints[0].Name()))
+ assert.Equal(t, domain.RouterHttp, entrypoints[0].Router())
+ assert.True(t, entrypoints[0].IsCustom())
+ assert.Equal(t, 80, entrypoints[0].Port())
+ assert.Equal(t, "my-app", entrypoints[0].Subdomain().Get(""))
+
+ assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-8080-http", appidLower), string(entrypoints[1].Name()))
+ assert.Equal(t, domain.RouterHttp, entrypoints[1].Router())
+ assert.False(t, entrypoints[1].IsCustom())
+ assert.Equal(t, 8080, entrypoints[1].Port())
+ assert.Equal(t, "my-app", entrypoints[1].Subdomain().Get(""))
+
+ assert.Equal(t, fmt.Sprintf("my-app-production-%s-other-3000-http", appidLower), string(entrypoints[2].Name()))
+ assert.Equal(t, domain.RouterHttp, entrypoints[2].Router())
+ assert.False(t, entrypoints[2].IsCustom())
+ assert.Equal(t, 3000, entrypoints[2].Port())
+ assert.Equal(t, "other.my-app", entrypoints[2].Subdomain().Get(""))
+ })
- testutil.HasLength(t, entrypoints, 3)
- testutil.Equals(t, http, entrypoints[0])
- testutil.Equals(t, udp, entrypoints[1])
- testutil.Equals(t, tcp, entrypoints[2])
+ t.Run("could have one or more TCP/UDP entrypoints attached", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ appidLower := strings.ToLower(string(app.ID()))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+
+ builder := deployment.Config().ServicesBuilder()
+
+ service := builder.AddService("app", "")
+
+ tcp := service.AddTCPEntrypoint(8080, true)
+ assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-8080-tcp", appidLower), string(tcp.Name()))
+ assert.Equal(t, domain.RouterTcp, tcp.Router())
+ assert.True(t, tcp.IsCustom())
+ assert.False(t, tcp.Subdomain().HasValue())
+ assert.Equal(t, 8080, tcp.Port())
+
+ udp := service.AddUDPEntrypoint(8080, true)
+ assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-8080-udp", appidLower), string(udp.Name()))
+ assert.Equal(t, domain.RouterUdp, udp.Router())
+ assert.True(t, udp.IsCustom())
+ assert.False(t, udp.Subdomain().HasValue())
+ assert.Equal(t, 8080, udp.Port())
+ })
+}
- entrypoints = services.CustomEntrypoints()
+func Test_Services(t *testing.T) {
- testutil.HasLength(t, entrypoints, 2)
- testutil.Equals(t, udp, entrypoints[0])
- testutil.Equals(t, tcp, entrypoints[1])
- })
+ t.Run("should be able to return all entrypoints", func(t *testing.T) {
+ deployment := fixture.Deployment()
+ builder := deployment.Config().ServicesBuilder()
- t.Run("should implement the valuer interface", func(t *testing.T) {
- var services domain.Services
+ service := builder.AddService("app", "")
+ http := service.AddHttpEntrypoint(80, false)
+ udp := service.AddUDPEntrypoint(8080, true)
+ service = builder.AddService("db", "postgres:14-alpine")
+ tcp := service.AddTCPEntrypoint(5432, true)
+ builder.AddService("cache", "redis:6-alpine")
+ services := builder.Services()
- s := config.NewService("app", "")
- s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{
- UseDefaultSubdomain: true,
- Managed: true,
- })
- s.AddTCPEntrypoint(8080)
+ entrypoints := services.Entrypoints()
- services = append(services, s)
+ assert.HasLength(t, 3, entrypoints)
+ assert.Equal(t, http, entrypoints[0])
+ assert.Equal(t, udp, entrypoints[1])
+ assert.Equal(t, tcp, entrypoints[2])
+ })
- s = config.NewService("db", "postgres:14-alpine")
- s.AddTCPEntrypoint(5432)
+ t.Run("should be able to return all custom entrypoints", func(t *testing.T) {
+ deployment := fixture.Deployment()
+ builder := deployment.Config().ServicesBuilder()
- services = append(services, s)
+ service := builder.AddService("app", "")
+ service.AddHttpEntrypoint(80, false)
+ udp := service.AddUDPEntrypoint(8080, true)
+ service = builder.AddService("db", "postgres:14-alpine")
+ tcp := service.AddTCPEntrypoint(5432, true)
+ builder.AddService("cache", "redis:6-alpine")
+ services := builder.Services()
- s = config.NewService("cache", "redis:6-alpine")
- services = append(services, s)
+ entrypoints := services.CustomEntrypoints()
- value, err := services.Value()
+ assert.HasLength(t, 2, entrypoints)
+ assert.Equal(t, udp, entrypoints[0])
+ assert.Equal(t, tcp, entrypoints[1])
+ })
- testutil.IsNil(t, err)
- testutil.Equals(t, fmt.Sprintf(`[{"name":"app","qualified_name":"my-app-production-%s-app","image":"my-app-%s/app:production","entrypoints":[{"name":"my-app-production-%s-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-%s-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","qualified_name":"my-app-production-%s-db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-%s-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","qualified_name":"my-app-production-%s-cache","image":"redis:6-alpine","entrypoints":[]}]`,
- appidLower, appidLower, appidLower, appidLower, appidLower, appidLower, appidLower), value.(string))
+ t.Run("should implement the valuer interface", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("my-app"))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ appidLower := strings.ToLower(string(deployment.ID().AppID()))
+ builder := deployment.Config().ServicesBuilder()
+
+ service := builder.AddService("app", "")
+ service.AddHttpEntrypoint(80, false)
+ service.AddTCPEntrypoint(8080, true)
+ service = builder.AddService("db", "postgres:14-alpine")
+ service.AddTCPEntrypoint(5432, true)
+ builder.AddService("cache", "redis:6-alpine")
+
+ value, err := builder.Services().Value()
+
+ assert.Nil(t, err)
+ assert.Equal(t, fmt.Sprintf(`[{"name":"app","image":"my-app-%s/app:production","entrypoints":[{"name":"my-app-production-%s-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-%s-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-%s-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","image":"redis:6-alpine","entrypoints":[]}]`,
+ appidLower, appidLower, appidLower, appidLower), value.(string))
})
t.Run("should implement the scanner interface", func(t *testing.T) {
@@ -194,13 +240,13 @@ func Test_Services(t *testing.T) {
}
]`)
- testutil.IsNil(t, err)
- testutil.HasLength(t, services, 3)
+ assert.Nil(t, err)
+ assert.HasLength(t, 3, services)
v, err := services.Value()
- testutil.IsNil(t, err)
- testutil.Equals(t, `[{"name":"app","qualified_name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app","image":"my-app-2fa8domd2sh7ehyqlxf7jvj57xs/app:production","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","qualified_name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","qualified_name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-cache","image":"redis:6-alpine","entrypoints":[]}]`, v.(string))
+ assert.Nil(t, err)
+ assert.Equal(t, `[{"name":"app","image":"my-app-2fa8domd2sh7ehyqlxf7jvj57xs/app:production","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","image":"redis:6-alpine","entrypoints":[]}]`, v.(string))
})
}
@@ -208,28 +254,28 @@ func Test_Port(t *testing.T) {
t.Run("should be able to parse a port from a raw string value", func(t *testing.T) {
_, err := domain.ParsePort("failed")
- testutil.ErrorIs(t, domain.ErrInvalidPort, err)
+ assert.ErrorIs(t, domain.ErrInvalidPort, err)
p, err := domain.ParsePort("8080")
- testutil.IsNil(t, err)
- testutil.Equals(t, 8080, p)
+ assert.Nil(t, err)
+ assert.Equal(t, 8080, p)
})
t.Run("should convert the port to a string", func(t *testing.T) {
p := domain.Port(8080)
- testutil.Equals(t, "8080", p.String())
+ assert.Equal(t, "8080", p.String())
})
t.Run("should convert the port to a uint32", func(t *testing.T) {
p := domain.Port(8080)
- testutil.Equals(t, 8080, p.Uint32())
+ assert.Equal(t, 8080, p.Uint32())
})
}
func Test_EntrypointName(t *testing.T) {
t.Run("should provide a protocol", func(t *testing.T) {
- testutil.Equals(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-http").Protocol())
- testutil.Equals(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp").Protocol())
- testutil.Equals(t, "udp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-udp").Protocol())
+ assert.Equal(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-http").Protocol())
+ assert.Equal(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp").Protocol())
+ assert.Equal(t, "udp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-udp").Protocol())
})
}
diff --git a/internal/deployment/domain/state.go b/internal/deployment/domain/state.go
deleted file mode 100644
index 95840a82..00000000
--- a/internal/deployment/domain/state.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package domain
-
-import (
- "time"
-
- "github.com/YuukanOO/seelf/pkg/apperr"
- "github.com/YuukanOO/seelf/pkg/monad"
-)
-
-var (
- ErrNotInPendingState = apperr.New("not_in_pending_state")
- ErrNotInRunningState = apperr.New("not_in_running_state")
-)
-
-const (
- DeploymentStatusPending DeploymentStatus = iota
- DeploymentStatusRunning
- DeploymentStatusFailed
- DeploymentStatusSucceeded
-)
-
-type (
- DeploymentStatus uint8
-
- // Holds together information related to the current deployment state. With a value
- // object, it is easier to validate consistency between all those related properties.
- // The default value represents a pending state.
- DeploymentState struct {
- status DeploymentStatus
- errcode monad.Maybe[string]
- services monad.Maybe[Services]
- startedAt monad.Maybe[time.Time]
- finishedAt monad.Maybe[time.Time]
- }
-)
-
-func (s *DeploymentState) Started() error {
- if s.status != DeploymentStatusPending {
- return ErrNotInPendingState
- }
-
- s.status = DeploymentStatusRunning
- s.startedAt.Set(time.Now().UTC())
-
- return nil
-}
-
-func (s *DeploymentState) Failed(err error) error {
- if s.status != DeploymentStatusRunning {
- return ErrNotInRunningState
- }
-
- s.status = DeploymentStatusFailed
- s.errcode.Set(err.Error())
- s.finishedAt.Set(time.Now().UTC())
-
- return nil
-}
-
-func (s *DeploymentState) Succeeded(services Services) error {
- if s.status != DeploymentStatusRunning {
- return ErrNotInRunningState
- }
-
- s.status = DeploymentStatusSucceeded
- s.services.Set(services)
- s.finishedAt.Set(time.Now().UTC())
-
- return nil
-}
-
-func (s DeploymentState) Status() DeploymentStatus { return s.status }
-func (s DeploymentState) ErrCode() monad.Maybe[string] { return s.errcode }
-func (s DeploymentState) Services() monad.Maybe[Services] { return s.services }
-func (s DeploymentState) StartedAt() monad.Maybe[time.Time] { return s.startedAt }
-func (s DeploymentState) FinishedAt() monad.Maybe[time.Time] { return s.finishedAt }
-
-const (
- TargetStatusConfiguring TargetStatus = iota
- TargetStatusFailed
- TargetStatusReady
-)
-
-type (
- TargetStatus uint8
-
- TargetState struct {
- status TargetStatus
- version time.Time
- errcode monad.Maybe[string]
- lastReadyVersion monad.Maybe[time.Time] // Hold down the last time the target was marked as ready
- }
-)
-
-func newTargetState() (t TargetState) {
- t.Reconfigure()
- return t
-}
-
-// Mark the state as configuring and update the version.
-func (t *TargetState) Reconfigure() {
- t.status = TargetStatusConfiguring
- t.version = time.Now().UTC()
- t.errcode.Unset()
-}
-
-// Update the state based on wether or not an error is given and returns a boolean indicating
-// if the state has changed.
-//
-// If there is no error, the target will be considered ready.
-// If an error is given, the target will be marked as failed.
-//
-// In either case, if the state has changed since it has been processed (the version param),
-// it will return without doing anything because the result is outdated.
-func (t *TargetState) Configured(version time.Time, err error) bool {
- if t.IsOutdated(version) {
- return false
- }
-
- if err != nil {
- t.status = TargetStatusFailed
- t.errcode.Set(err.Error())
- return true
- }
-
- t.status = TargetStatusReady
- t.lastReadyVersion.Set(version)
- t.errcode.Unset()
-
- return true
-}
-
-// Returns true if the given version is different from the current one or if the one
-// provided is already configured.
-func (t TargetState) IsOutdated(version time.Time) bool {
- return version != t.version || t.status != TargetStatusConfiguring
-}
-
-func (t TargetState) Status() TargetStatus { return t.status }
-func (t TargetState) ErrCode() monad.Maybe[string] { return t.errcode }
-func (t TargetState) Version() time.Time { return t.version }
-func (t TargetState) LastReadyVersion() monad.Maybe[time.Time] { return t.lastReadyVersion }
diff --git a/internal/deployment/domain/state_test.go b/internal/deployment/domain/state_test.go
deleted file mode 100644
index 39a937f0..00000000
--- a/internal/deployment/domain/state_test.go
+++ /dev/null
@@ -1,169 +0,0 @@
-package domain_test
-
-import (
- "errors"
- "testing"
-
- "github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
-)
-
-func Test_DeploymentState(t *testing.T) {
- t.Run("should be created in pending state", func(t *testing.T) {
- var state domain.DeploymentState
-
- testutil.Equals(t, domain.DeploymentStatusPending, state.Status())
- testutil.IsFalse(t, state.ErrCode().HasValue())
- testutil.IsFalse(t, state.Services().HasValue())
- testutil.IsFalse(t, state.StartedAt().HasValue())
- testutil.IsFalse(t, state.FinishedAt().HasValue())
- })
-
- t.Run("could be marked as started", func(t *testing.T) {
- var (
- state domain.DeploymentState
- err error
- )
-
- err = state.Started()
-
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.DeploymentStatusRunning, state.Status())
- testutil.IsTrue(t, state.StartedAt().HasValue())
- testutil.IsFalse(t, state.FinishedAt().HasValue())
- })
-
- t.Run("could fail", func(t *testing.T) {
- var (
- state domain.DeploymentState
- err error
- )
-
- testutil.IsNil(t, state.Started())
-
- err = state.Failed(errors.New("some error"))
-
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.DeploymentStatusFailed, state.Status())
- testutil.Equals(t, "some error", state.ErrCode().MustGet())
- testutil.IsTrue(t, state.StartedAt().HasValue())
- testutil.IsTrue(t, state.FinishedAt().HasValue())
- })
-
- t.Run("could succeed", func(t *testing.T) {
- var (
- state domain.DeploymentState
- err error
- )
-
- app := must.Panic(domain.NewApp("app1",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true),
- "uid"))
- conf := must.Panic(app.ConfigSnapshotFor(domain.Production))
- services := domain.Services{
- conf.NewService("app", ""),
- }
- testutil.IsNil(t, state.Started())
-
- err = state.Succeeded(services)
-
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.DeploymentStatusSucceeded, state.Status())
- testutil.IsFalse(t, state.ErrCode().HasValue())
- testutil.IsTrue(t, state.Services().HasValue())
- testutil.DeepEquals(t, services, state.Services().MustGet())
- testutil.IsTrue(t, state.StartedAt().HasValue())
- testutil.IsTrue(t, state.FinishedAt().HasValue())
- })
-
- t.Run("should err if trying to start but not in pending state", func(t *testing.T) {
- var state domain.DeploymentState
- testutil.IsNil(t, state.Started())
-
- err := state.Started()
-
- testutil.ErrorIs(t, domain.ErrNotInPendingState, err)
- })
-
- t.Run("should err if trying to fail but not in runing state", func(t *testing.T) {
- var state domain.DeploymentState
-
- err := state.Failed(errors.New("an error"))
-
- testutil.ErrorIs(t, domain.ErrNotInRunningState, err)
- })
-
- t.Run("should err if trying to succeed but not in runing state", func(t *testing.T) {
- var state domain.DeploymentState
-
- err := state.Succeeded(domain.Services{})
-
- testutil.ErrorIs(t, domain.ErrNotInRunningState, err)
- })
-}
-
-func Test_TargetState(t *testing.T) {
- t.Run("should be created in configuring state", func(t *testing.T) {
- var state domain.TargetState
-
- testutil.Equals(t, domain.TargetStatusConfiguring, state.Status())
- testutil.IsTrue(t, state.Version().IsZero())
- testutil.IsFalse(t, state.ErrCode().HasValue())
- testutil.IsFalse(t, state.LastReadyVersion().HasValue())
- })
-
- t.Run("can be reconfigured", func(t *testing.T) {
- var state domain.TargetState
-
- state.Reconfigure()
-
- testutil.Equals(t, domain.TargetStatusConfiguring, state.Status())
- testutil.IsFalse(t, state.Version().IsZero())
- testutil.IsFalse(t, state.ErrCode().HasValue())
- })
-
- t.Run("could be marked has done and sets the errcode and status appropriately", func(t *testing.T) {
- var (
- state domain.TargetState
- errFailed = errors.New("failed")
- )
- state.Reconfigure()
-
- testutil.IsTrue(t, state.Configured(state.Version(), errFailed))
-
- testutil.Equals(t, domain.TargetStatusFailed, state.Status())
- testutil.Equals(t, errFailed.Error(), state.ErrCode().MustGet())
- testutil.IsFalse(t, state.LastReadyVersion().HasValue())
-
- state.Reconfigure()
-
- testutil.IsTrue(t, state.Configured(state.Version(), nil))
- testutil.Equals(t, state.Version(), state.LastReadyVersion().MustGet())
-
- testutil.Equals(t, domain.TargetStatusReady, state.Status())
- testutil.IsFalse(t, state.ErrCode().HasValue())
- })
-
- t.Run("should do nothing if the version does not match or if it has been already configured", func(t *testing.T) {
- var state domain.TargetState
- state.Reconfigure()
-
- testutil.IsFalse(t, state.Configured(state.Version().Add(-1), nil))
-
- testutil.Equals(t, domain.TargetStatusConfiguring, state.Status())
- testutil.IsFalse(t, state.ErrCode().HasValue())
- testutil.IsFalse(t, state.Version().IsZero())
- testutil.IsFalse(t, state.LastReadyVersion().HasValue())
-
- state.Configured(state.Version(), nil)
-
- testutil.IsFalse(t, state.Configured(state.Version(), errors.New("should not happen")))
-
- testutil.Equals(t, domain.TargetStatusReady, state.Status())
- testutil.Equals(t, state.Version(), state.LastReadyVersion().MustGet())
- testutil.IsFalse(t, state.ErrCode().HasValue())
- testutil.IsFalse(t, state.Version().IsZero())
- })
-}
diff --git a/internal/deployment/domain/target.go b/internal/deployment/domain/target.go
index 149a3310..c09af041 100644
--- a/internal/deployment/domain/target.go
+++ b/internal/deployment/domain/target.go
@@ -28,7 +28,13 @@ var (
const (
CleanupStrategyDefault CleanupStrategy = iota // Default strategy, try to remove the target data but returns an error if it fails
- CleanupStrategySkip // Skip the cleanup because no resource has been deployed
+ CleanupStrategySkip // Skip the cleanup because no resource has been deployed or we can't remove them anymore
+)
+
+const (
+ TargetStatusConfiguring TargetStatus = iota
+ TargetStatusFailed
+ TargetStatusReady
)
type (
@@ -43,7 +49,7 @@ type (
id TargetID
name string
- url Url
+ url monad.Maybe[Url]
provider ProviderConfig
state TargetState
customEntrypoints TargetEntrypoints
@@ -67,7 +73,6 @@ type (
ID TargetID
Name string
- Url Url
Provider ProviderConfig
State TargetState
Entrypoints TargetEntrypoints
@@ -95,6 +100,12 @@ type (
Url Url
}
+ TargetUrlRemoved struct {
+ bus.Notification
+
+ ID TargetID
+ }
+
TargetProviderChanged struct {
bus.Notification
@@ -127,6 +138,7 @@ func (TargetCreated) Name_() string { return "deployment.event.target
func (TargetStateChanged) Name_() string { return "deployment.event.target_state_changed" }
func (TargetRenamed) Name_() string { return "deployment.event.target_renamed" }
func (TargetUrlChanged) Name_() string { return "deployment.event.target_url_changed" }
+func (TargetUrlRemoved) Name_() string { return "deployment.event.target_url_removed" }
func (TargetProviderChanged) Name_() string { return "deployment.event.target_provider_changed" }
func (TargetEntrypointsChanged) Name_() string { return "deployment.event.target_entrypoints_changed" }
func (TargetCleanupRequested) Name_() string { return "deployment.event.target_cleanup_requested" }
@@ -139,16 +151,9 @@ func (e TargetStateChanged) WentToConfiguringState() bool {
// Builds a new deployment target.
func NewTarget(
name string,
- urlRequirement TargetUrlRequirement,
providerRequirement ProviderConfigRequirement,
createdBy auth.UserID,
) (t Target, err error) {
- url, err := urlRequirement.Met()
-
- if err != nil {
- return t, err
- }
-
provider, err := providerRequirement.Met()
if err != nil {
@@ -158,7 +163,6 @@ func NewTarget(
t.apply(TargetCreated{
ID: id.New[TargetID](),
Name: name,
- Url: url.Root(),
Provider: provider,
State: newTargetState(),
Entrypoints: make(TargetEntrypoints),
@@ -229,8 +233,8 @@ func (t *Target) Rename(name string) error {
return nil
}
-// Update the internal domain used by this target.
-func (t *Target) HasUrl(urlRequirement TargetUrlRequirement) error {
+// Mark this target as exposing automatically services on the given root url.
+func (t *Target) ExposeServicesAutomatically(urlRequirement TargetUrlRequirement) error {
if t.cleanupRequested.HasValue() {
return ErrTargetCleanupRequested
}
@@ -241,13 +245,35 @@ func (t *Target) HasUrl(urlRequirement TargetUrlRequirement) error {
return err
}
- if t.url == url {
+ url = url.Root() // Remove path and query part
+
+ if existing, isSet := t.url.TryGet(); isSet && existing == url {
return nil
}
t.apply(TargetUrlChanged{
ID: t.id,
- Url: url.Root(),
+ Url: url,
+ })
+
+ t.reconfigure()
+
+ return nil
+}
+
+// Mark this target as being manually managed by the user. The url will be removed
+// and the user will have to manually manage the proxy configuration.
+func (t *Target) ExposeServicesManually() error {
+ if t.cleanupRequested.HasValue() {
+ return ErrTargetCleanupRequested
+ }
+
+ if !t.url.HasValue() {
+ return nil
+ }
+
+ t.apply(TargetUrlRemoved{
+ ID: t.id,
})
t.reconfigure()
@@ -324,7 +350,7 @@ func (t *Target) Reconfigure() error {
// Mark the target (in the given version) has configured (by an external system).
// If the given version does not match the current one, nothing will be done.
func (t *Target) Configured(version time.Time, assigned TargetEntrypointsAssigned, err error) {
- if !t.state.Configured(version, err) {
+ if !t.state.configured(version, err) {
return
}
@@ -347,7 +373,7 @@ func (t *Target) Configured(version time.Time, assigned TargetEntrypointsAssigne
// If needed (new or removed entrypoints), a configuration will be triggered.
func (t *Target) ExposeEntrypoints(app AppID, env Environment, services Services) {
// Target is being deleted, no need to reconfigure anything
- if t.cleanupRequested.HasValue() || services == nil {
+ if t.cleanupRequested.HasValue() {
return
}
@@ -461,18 +487,19 @@ func (t *Target) Delete(cleanedUp bool) error {
}
func (t *Target) ID() TargetID { return t.id }
-func (t *Target) Url() Url { return t.url }
+func (t *Target) Url() monad.Maybe[Url] { return t.url }
+func (t *Target) IsManual() bool { return !t.url.HasValue() }
func (t *Target) Provider() ProviderConfig { return t.provider }
func (t *Target) CustomEntrypoints() TargetEntrypoints { return t.customEntrypoints } // FIXME: Should we return a copy?
func (t *Target) CurrentVersion() time.Time { return t.state.version }
// Returns true if the given configuration version is different from the current one.
func (t *Target) IsOutdated(version time.Time) bool {
- return t.state.IsOutdated(version)
+ return t.state.isOutdated(version)
}
func (t *Target) reconfigure() {
- t.state.Reconfigure()
+ t.state.reconfigure()
t.apply(TargetStateChanged{
ID: t.id,
@@ -486,6 +513,10 @@ func (t *Target) raiseEntrypointsChangedAndReconfigure() {
Entrypoints: t.customEntrypoints,
})
+ if t.IsManual() {
+ return
+ }
+
t.reconfigure()
}
@@ -494,7 +525,6 @@ func (t *Target) apply(e event.Event) {
case TargetCreated:
t.id = evt.ID
t.name = evt.Name
- t.url = evt.Url
t.provider = evt.Provider
t.state = evt.State
t.created = evt.Created
@@ -502,7 +532,9 @@ func (t *Target) apply(e event.Event) {
case TargetRenamed:
t.name = evt.Name
case TargetUrlChanged:
- t.url = evt.Url
+ t.url.Set(evt.Url)
+ case TargetUrlRemoved:
+ t.url.Unset()
case TargetProviderChanged:
t.provider = evt.Provider
case TargetEntrypointsChanged:
@@ -516,6 +548,66 @@ func (t *Target) apply(e event.Event) {
event.Store(t, e)
}
+type (
+ TargetStatus uint8
+
+ TargetState struct {
+ status TargetStatus
+ version time.Time
+ errcode monad.Maybe[string]
+ lastReadyVersion monad.Maybe[time.Time] // Hold down the last time the target was marked as ready
+ }
+)
+
+func newTargetState() (t TargetState) {
+ t.reconfigure()
+ return t
+}
+
+// Mark the state as configuring and update the version.
+func (t *TargetState) reconfigure() {
+ t.status = TargetStatusConfiguring
+ t.version = time.Now().UTC()
+ t.errcode.Unset()
+}
+
+// Update the state based on wether or not an error is given and returns a boolean indicating
+// if the state has changed.
+//
+// If there is no error, the target will be considered ready.
+// If an error is given, the target will be marked as failed.
+//
+// In either case, if the state has changed since it has been processed (the version param),
+// it will return without doing anything because the result is outdated.
+func (t *TargetState) configured(version time.Time, err error) bool {
+ if t.isOutdated(version) {
+ return false
+ }
+
+ if err != nil {
+ t.status = TargetStatusFailed
+ t.errcode.Set(err.Error())
+ return true
+ }
+
+ t.status = TargetStatusReady
+ t.lastReadyVersion.Set(version)
+ t.errcode.Unset()
+
+ return true
+}
+
+// Returns true if the given version is different from the current one or if the one
+// provided is already configured.
+func (t TargetState) isOutdated(version time.Time) bool {
+ return version != t.version || t.status != TargetStatusConfiguring
+}
+
+func (t TargetState) Status() TargetStatus { return t.status }
+func (t TargetState) ErrCode() monad.Maybe[string] { return t.errcode }
+func (t TargetState) Version() time.Time { return t.version }
+func (t TargetState) LastReadyVersion() monad.Maybe[time.Time] { return t.lastReadyVersion }
+
func (e TargetEntrypoints) Value() (driver.Value, error) { return storage.ValueJSON(e) }
func (e *TargetEntrypoints) Scan(value any) error { return storage.ScanJSON(value, e) }
diff --git a/internal/deployment/domain/target_test.go b/internal/deployment/domain/target_test.go
index d17ec11d..1fcf3269 100644
--- a/internal/deployment/domain/target_test.go
+++ b/internal/deployment/domain/target_test.go
@@ -7,675 +7,875 @@ import (
auth "github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+ shared "github.com/YuukanOO/seelf/pkg/domain"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Target(t *testing.T) {
- var (
- name = "my-target"
- targetUrl = must.Panic(domain.UrlFrom("http://my-url.com"))
- config domain.ProviderConfig = dummyProviderConfig{}
- uid auth.UserID = "uid"
-
- urlNotUnique = domain.NewTargetUrlRequirement(targetUrl, false)
- urlUnique = domain.NewTargetUrlRequirement(targetUrl, true)
- configNotUnique = domain.NewProviderConfigRequirement(config, false)
- configUnique = domain.NewProviderConfigRequirement(config, true)
- app = must.Panic(domain.NewApp("my-app",
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true),
- "uid"))
- deployConfig = must.Panic(app.ConfigSnapshotFor(domain.Production))
- )
-
- t.Run("should fail if the url is not unique", func(t *testing.T) {
- _, err := domain.NewTarget(name, urlNotUnique, configUnique, uid)
- testutil.Equals(t, domain.ErrUrlAlreadyTaken, err)
- })
-
- t.Run("should fail if the config is not unique", func(t *testing.T) {
- _, err := domain.NewTarget(name, urlUnique, configNotUnique, uid)
- testutil.Equals(t, domain.ErrConfigAlreadyTaken, err)
- })
-
- t.Run("should be instantiable", func(t *testing.T) {
- target, err := domain.NewTarget(name, urlUnique, configUnique, uid)
-
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &target, 1)
- evt := testutil.EventIs[domain.TargetCreated](t, &target, 0)
-
- testutil.NotEquals(t, "", evt.ID)
- testutil.Equals(t, name, evt.Name)
- testutil.Equals(t, targetUrl.String(), evt.Url.String())
- testutil.Equals(t, config, evt.Provider)
- testutil.Equals(t, domain.TargetStatusConfiguring, evt.State.Status())
- testutil.Equals(t, uid, evt.Created.By())
- })
-
- t.Run("could be renamed and raise the event only if different", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
-
- err := target.Rename("new-name")
-
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.TargetRenamed](t, &target, 1)
- testutil.Equals(t, "new-name", evt.Name)
-
- testutil.IsNil(t, target.Rename("new-name"))
- testutil.HasNEvents(t, &target, 2)
- })
-
- t.Run("could not be renamed if delete requested", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- testutil.IsNil(t, target.RequestCleanup(false, uid))
-
- testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Rename("new-name"))
- })
-
- t.Run("could have its domain changed if available and raise the event only if different", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- newUrl := must.Panic(domain.UrlFrom("http://new-url.com"))
- err := target.HasUrl(domain.NewTargetUrlRequirement(newUrl, false))
-
- testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, err)
-
- err = target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true))
-
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.TargetUrlChanged](t, &target, 1)
- testutil.Equals(t, newUrl.String(), evt.Url.String())
-
- evtTargetChanged := testutil.EventIs[domain.TargetStateChanged](t, &target, 2)
- testutil.Equals(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status())
-
- testutil.IsNil(t, target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true)))
- testutil.HasNEvents(t, &target, 3)
- })
-
- t.Run("could not have its domain changed if delete requested", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
-
- newUrl := domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://new-url.com")), true)
-
- testutil.IsNil(t, target.RequestCleanup(false, uid))
- testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.HasUrl(newUrl))
- })
-
- t.Run("should forbid a provider change if the fingerprint has changed", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name,
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true), configUnique, uid))
-
- err := target.HasProvider(domain.NewProviderConfigRequirement(dummyProviderConfig{data: "new-config", fingerprint: "new-fingerprint"}, true))
-
- testutil.ErrorIs(t, domain.ErrTargetProviderUpdateNotPermitted, err)
- })
-
- t.Run("could have its provider changed if available and raise the event only if different", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name,
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true),
- configUnique, uid))
- newConfig := dummyProviderConfig{data: "new-config"}
-
- err := target.HasProvider(domain.NewProviderConfigRequirement(newConfig, false))
-
- testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, err)
-
- err = target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true))
-
- testutil.IsNil(t, err)
- evt := testutil.EventIs[domain.TargetProviderChanged](t, &target, 1)
- testutil.IsTrue(t, newConfig == evt.Provider)
-
- evtTargetChanged := testutil.EventIs[domain.TargetStateChanged](t, &target, 2)
- testutil.Equals(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status())
+ // Common data used for custom entrypoints exposure
+ deployment := fixture.Deployment()
+ builder := deployment.Config().ServicesBuilder()
+ app := builder.AddService("app", "app-image")
+ app.AddHttpEntrypoint(80, false)
+ http := app.AddHttpEntrypoint(3000, true)
+ db := builder.AddService("db", "db-image")
+ tcp := db.AddTCPEntrypoint(5432, true)
+
+ t.Run("could be created", func(t *testing.T) {
+ t.Run("should require a unique provider config", func(t *testing.T) {
+ _, err := domain.NewTarget("target",
+ domain.NewProviderConfigRequirement(fixture.ProviderConfig(), false), "uid")
+
+ assert.ErrorIs(t, domain.ErrConfigAlreadyTaken, err)
+ })
+
+ t.Run("should succeed if everything is good", func(t *testing.T) {
+ config := fixture.ProviderConfig()
+
+ target, err := domain.NewTarget("target",
+ domain.NewProviderConfigRequirement(config, true),
+ "uid")
+
+ assert.Nil(t, err)
+ assert.Equal(t, config, target.Provider())
+ assert.Zero(t, target.Url())
+ assert.HasNEvents(t, 1, &target)
+ created := assert.EventIs[domain.TargetCreated](t, &target, 0)
+
+ assert.DeepEqual(t, domain.TargetCreated{
+ ID: assert.NotZero(t, target.ID()),
+ Name: "target",
+ Provider: config,
+ State: created.State,
+ Entrypoints: make(domain.TargetEntrypoints),
+ Created: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, created.Created.At())),
+ }, created)
+
+ assert.Equal(t, domain.TargetStatusConfiguring, created.State.Status())
+ assert.Equal(t, target.CurrentVersion(), created.State.Version())
+ assert.Zero(t, created.State.ErrCode())
+ assert.Zero(t, created.State.LastReadyVersion())
+ })
+ })
+
+ t.Run("should expose a method to check if a version is outdated or not", func(t *testing.T) {
+ t.Run("should return true if the version is outdated", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.True(t, target.IsOutdated(target.CurrentVersion().Add(-1*time.Second)))
+ })
+
+ t.Run("should return true if the target is not in a configuring state", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+
+ assert.True(t, target.IsOutdated(target.CurrentVersion()))
+ })
+
+ t.Run("should return false if the version is not outdated", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.False(t, target.IsOutdated(target.CurrentVersion()))
+ })
+ })
+
+ t.Run("could be renamed", func(t *testing.T) {
+ t.Run("should not raise the event if the name has not changed", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetName("name"))
+
+ assert.Nil(t, target.Rename("name"))
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should raise the event if the name is different", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetName("old-name"))
+
+ assert.Nil(t, target.Rename("new-name"))
+ assert.HasNEvents(t, 2, &target)
+ renamed := assert.EventIs[domain.TargetRenamed](t, &target, 1)
+ assert.Equal(t, domain.TargetRenamed{
+ ID: target.ID(),
+ Name: "new-name",
+ }, renamed)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetName("old-name"))
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Rename("new-name"))
+ })
+ })
+
+ t.Run("could be configured as exposing services automatically with an url", func(t *testing.T) {
+ t.Run("should require the url to be unique", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, target.ExposeServicesAutomatically(
+ domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), false),
+ ))
+ })
+
+ t.Run("should raise the event if the url is different", func(t *testing.T) {
+ target := fixture.Target()
+ url := must.Panic(domain.UrlFrom("http://example.com"))
+
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(url, true)))
+
+ assert.HasNEvents(t, 3, &target)
+ urlChanged := assert.EventIs[domain.TargetUrlChanged](t, &target, 1)
+ assert.Equal(t, domain.TargetUrlChanged{
+ ID: target.ID(),
+ Url: url,
+ }, urlChanged)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+
+ t.Run("should not raise the event if the url has not changed", func(t *testing.T) {
+ target := fixture.Target()
+ url := must.Panic(domain.UrlFrom("http://example.com"))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(url, true)))
+
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(url, true)))
+ assert.HasNEvents(t, 3, &target)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.ExposeServicesAutomatically(
+ domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true),
+ ))
+ })
+ })
+
+ t.Run("could be configured as exposing services manually without url", func(t *testing.T) {
+ t.Run("should raise the event if the target had previously an url", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
+
+ assert.Nil(t, target.ExposeServicesManually())
+
+ assert.HasNEvents(t, 5, &target)
+ urlRemoved := assert.EventIs[domain.TargetUrlRemoved](t, &target, 3)
+ assert.Equal(t, domain.TargetUrlRemoved{
+ ID: target.ID(),
+ }, urlRemoved)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 4)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
- testutil.IsNil(t, target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true)))
- testutil.HasNEvents(t, &target, 3)
- })
-
- t.Run("could not have its provider changed if delete requested", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
-
- testutil.IsNil(t, target.RequestCleanup(false, uid))
- testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.HasProvider(configUnique))
- })
-
- t.Run("could be marked as configured and raise the appropriate event", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
-
- target.Configured(target.CurrentVersion().Add(-1*time.Hour), nil, nil)
-
- testutil.HasNEvents(t, &target, 1)
- testutil.EventIs[domain.TargetCreated](t, &target, 0)
-
- target.Configured(target.CurrentVersion(), nil, nil)
- target.Configured(target.CurrentVersion(), nil, nil) // Should not raise a new event
-
- testutil.HasNEvents(t, &target, 2)
- changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 1)
- testutil.Equals(t, domain.TargetStatusReady, changed.State.Status())
- })
-
- t.Run("should handle entrypoints assignment on configuration", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
+ t.Run("should not raise the event if trying to remove an url on a target without one", func(t *testing.T) {
+ target := fixture.Target()
- // Assigning non existing entrypoints should just be ignored
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- app.ID(): {
- domain.Production: {
- "non-existing-entrypoint": 5432,
+ assert.Nil(t, target.ExposeServicesManually())
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.ExposeServicesManually())
+ })
+ })
+
+ t.Run("could have its provider changed", func(t *testing.T) {
+ t.Run("should require the provider to be unique", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.ErrorIs(t, domain.ErrConfigAlreadyTaken,
+ target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(), false)))
+ })
+
+ t.Run("should require the fingerprint to be the same", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+
+ assert.ErrorIs(t, domain.ErrTargetProviderUpdateNotPermitted,
+ target.HasProvider(
+ domain.NewProviderConfigRequirement(
+ fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("456")), true)))
+ })
+
+ t.Run("should require the provider kind to be the same", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test1"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+
+ assert.ErrorIs(t, domain.ErrTargetProviderUpdateNotPermitted,
+ target.HasProvider(
+ domain.NewProviderConfigRequirement(
+ fixture.ProviderConfig(fixture.WithKind("test2"), fixture.WithFingerprint("123")), true)))
+ })
+
+ t.Run("should raise the event if the provider is different", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+ newConfig := fixture.ProviderConfig(
+ fixture.WithKind("test"),
+ fixture.WithFingerprint("123"),
+ fixture.WithData("some different data"))
+
+ assert.Nil(t, target.HasProvider(
+ domain.NewProviderConfigRequirement(newConfig, true)))
+ assert.HasNEvents(t, 3, &target)
+ changed := assert.EventIs[domain.TargetProviderChanged](t, &target, 1)
+ assert.Equal(t, domain.TargetProviderChanged{
+ ID: target.ID(),
+ Provider: newConfig,
+ }, changed)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+
+ t.Run("should not raise the event if the provider is the same", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+
+ assert.Nil(t, target.HasProvider(domain.NewProviderConfigRequirement(config, true)))
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(), true)))
+ })
+ })
+
+ t.Run("could expose custom entrypoints", func(t *testing.T) {
+ t.Run("should do nothing if given entrypoints are empty", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{})
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should do nothing if given entrypoints are nil", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, nil)
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should add entrypoints", func(t *testing.T) {
+ t.Run("on manual target", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+
+ assert.HasNEvents(t, 2, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 1)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ })
+
+ t.Run("on automatic target", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+
+ assert.HasNEvents(t, 5, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 4)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+ })
+
+ t.Run("should update existing entrypoints", func(t *testing.T) {
+ t.Run("on manual target", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service})
+
+ assert.HasNEvents(t, 3, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 2)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ })
+
+ t.Run("on automatic target", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service})
+
+ assert.HasNEvents(t, 7, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 5)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 6)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+ })
+
+ t.Run("should not raise additional events if all entrypoints already exists", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+ assert.HasNEvents(t, 2, &target)
+ })
+
+ t.Run("should be ignored if the target is being configured", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+
+ assert.HasNEvents(t, 3, &target)
+ })
+ })
+
+ t.Run("could be marked as configured", func(t *testing.T) {
+ t.Run("should do nothing if the version does not match", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+
+ target.Configured(target.CurrentVersion().Add(-1*time.Second), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
},
- },
- }, nil)
+ }, nil)
- testutil.HasNEvents(t, &target, 2)
- testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
+ assert.HasNEvents(t, 2, &target)
+ })
- dbService := deployConfig.NewService("db", "postgres:14-alpine")
- http := dbService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{})
- tcp := dbService.AddTCPEntrypoint(5432)
+ t.Run("should do nothing if the version has already been configured", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService})
+ target.Configured(target.CurrentVersion(), nil, nil)
- // Assigning but with an error should ignore new entrypoints
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- app.ID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
- },
- },
- }, errors.New("some error"))
-
- testutil.HasNEvents(t, &target, 5)
- testutil.DeepEquals(t, domain.TargetEntrypoints{
- app.ID(): {
- domain.Production: {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, target.CustomEntrypoints())
+ assert.HasNEvents(t, 2, &target)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 1)
+ assert.Equal(t, domain.TargetStatusReady, stateChanged.State.Status())
+ })
- testutil.IsNil(t, target.Reconfigure())
+ t.Run("should be marked as failed if an error is given", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+ err := errors.New("an error")
- // No error, should update the entrypoints correctly
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- app.ID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
- "non-existing-entrypoint": 5432,
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
},
- "non-existing-env": {
- "non-existing-entrypoint": 5432,
+ }, err)
+
+ assert.HasNEvents(t, 3, &target)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusFailed, stateChanged.State.Status())
+ assert.Equal(t, err.Error(), stateChanged.State.ErrCode().Get(""))
+ assert.Zero(t, stateChanged.State.LastReadyVersion())
+ })
+
+ t.Run("should be marked as ready and update entrypoints with given assigned ports ignoring non-existing entrypoints", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service})
+
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ "another-app": {
+ domain.Production: {
+ "some-entrypoint": 5000,
+ },
},
- },
- "another-app": {
- "non-existing-env": {
- "non-existing-entrypoint": 5432,
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
},
- },
- }, nil)
-
- testutil.HasNEvents(t, &target, 8)
- testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 6)
- changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 7)
- testutil.Equals(t, domain.TargetStatusReady, changed.State.Status())
- testutil.DeepEquals(t, domain.TargetEntrypoints{
- app.ID(): {
- domain.Production: {
- http.Name(): monad.Value[domain.Port](8081),
- tcp.Name(): monad.Value[domain.Port](8082),
+ }, nil)
+
+ assert.HasNEvents(t, 5, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Staging: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ domain.Production: {
+ http.Name(): monad.Value[domain.Port](3000),
+ tcp.Name(): monad.Value[domain.Port](3001),
+ },
+ },
},
- },
- }, target.CustomEntrypoints())
- })
-
- t.Run("should be able to unexpose entrypoints for a specific app", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- dbService := deployConfig.NewService("db", "postgres:14-alpine")
- http := dbService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{})
- tcp := dbService.AddTCPEntrypoint(5432)
-
- target.UnExposeEntrypoints(app.ID())
-
- testutil.HasNEvents(t, &target, 1)
-
- target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService})
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- app.ID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
+ }, entrypointsChanged)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 4)
+ assert.Equal(t, domain.TargetStatusReady, stateChanged.State.Status())
+ assert.Zero(t, stateChanged.State.ErrCode())
+ assert.Equal(t, target.CurrentVersion(), stateChanged.State.LastReadyVersion().Get(time.Time{}))
+ })
+ })
+
+ t.Run("could un-expose custom entrypoints", func(t *testing.T) {
+ t.Run("should do nothing if not previously exposed", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should un-expose all entrypoints of a given application", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints("app", domain.Production, domain.Services{app.Service, db.Service})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ domain.Staging: {
+ http.Name(): 3002,
+ tcp.Name(): 3003,
+ },
},
- },
- }, nil)
-
- target.UnExposeEntrypoints(app.ID())
-
- testutil.HasNEvents(t, &target, 7)
- testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
- changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 6)
- testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status())
-
- target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService})
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- app.ID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
+ }, nil)
+
+ target.UnExposeEntrypoints(deployment.Config().AppID())
+
+ assert.HasNEvents(t, 7, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 6)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ "app": {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ },
},
- },
- }, nil)
+ }, entrypointsChanged)
+ })
+
+ t.Run("should un-expose all entrypoints of an application for a specific environment", func(t *testing.T) {
+ t.Run("on manual target", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ domain.Staging: {
+ http.Name(): 3002,
+ tcp.Name(): 3003,
+ },
+ },
+ }, nil)
+
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
+
+ assert.HasNEvents(t, 6, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 5)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Staging: {
+ http.Name(): monad.Value[domain.Port](3002),
+ tcp.Name(): monad.Value[domain.Port](3003),
+ },
+ },
+ },
+ }, entrypointsChanged)
+ })
+
+ t.Run("on automatic target", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("https://example.com")), true)))
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ domain.Staging: {
+ http.Name(): 3002,
+ tcp.Name(): 3003,
+ },
+ },
+ }, nil)
+
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
+
+ assert.HasNEvents(t, 11, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 9)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Staging: {
+ http.Name(): monad.Value[domain.Port](3002),
+ tcp.Name(): monad.Value[domain.Port](3003),
+ },
+ },
+ },
+ }, entrypointsChanged)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 10)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+ })
+
+ t.Run("should be ignored if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ },
+ }, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- target.UnExposeEntrypoints(app.ID(), domain.Staging)
- target.UnExposeEntrypoints(app.ID(), domain.Production)
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
- testutil.HasNEvents(t, &target, 13)
- testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
- changed = testutil.EventIs[domain.TargetStateChanged](t, &target, 12)
- testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status())
+ assert.HasNEvents(t, 5, &target)
+ })
})
t.Run("could expose its availability based on its internal state", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
-
- // Configuring
- err := target.CheckAvailability()
-
- testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
-
- // Configuration failed
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
-
- err = target.CheckAvailability()
-
- testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
-
- // Configuration success
- target.Reconfigure()
-
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("when configuring", func(t *testing.T) {
+ target := fixture.Target()
- err = target.CheckAvailability()
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.CheckAvailability())
+ })
- testutil.IsNil(t, err)
+ t.Run("when configuration failed", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
- // Delete requested
- target.RequestCleanup(false, uid)
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, target.CheckAvailability())
+ })
- err = target.CheckAvailability()
+ t.Run("when ready", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, err)
- })
+ assert.Nil(t, target.CheckAvailability())
+ })
- t.Run("could not be reconfigured if cleanup requested", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- testutil.IsNil(t, target.RequestCleanup(false, uid))
+ t.Run("when cleanup requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure())
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.CheckAvailability())
+ })
})
- t.Run("could not be reconfigured if configuring", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
+ t.Run("could be reconfigured", func(t *testing.T) {
+ t.Run("should fail if already being configured", func(t *testing.T) {
+ target := fixture.Target()
- testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.Reconfigure())
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.Reconfigure())
+ })
- t.Run("should not be removed if still used by an app", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should fail if cleanup requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- testutil.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, uid))
- })
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure())
+ })
- t.Run("should not be removed if configuring", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
-
- testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, uid))
- })
+ t.Run("should succeed otherwise", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- t.Run("could be removed if no app is using it", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.Reconfigure())
- err := target.RequestCleanup(false, uid)
- testutil.IsNil(t, err)
-
- testutil.IsNil(t, err)
- testutil.HasNEvents(t, &target, 3)
- evt := testutil.EventIs[domain.TargetCleanupRequested](t, &target, 2)
- testutil.Equals(t, target.ID(), evt.ID)
+ assert.HasNEvents(t, 3, &target)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
})
- t.Run("should not raise an event is the target is already marked has deleting", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
-
- testutil.IsNil(t, target.RequestCleanup(false, uid))
- testutil.IsNil(t, target.RequestCleanup(false, uid))
-
- testutil.HasNEvents(t, &target, 3)
- })
+ t.Run("could be marked for cleanup", func(t *testing.T) {
+ t.Run("should returns an err if some applications are using it", func(t *testing.T) {
+ target := fixture.Target()
- t.Run("should returns an err if trying to cleanup a target while configuring", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
+ assert.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, "uid"))
+ })
- _, err := target.CleanupStrategy(false)
+ t.Run("should returns an err if configuring", func(t *testing.T) {
+ target := fixture.Target()
- testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, "uid"))
+ })
- t.Run("should returns an err if trying to cleanup a target while deployments are still running", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should succeed otherwise", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- _, err := target.CleanupStrategy(true)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- testutil.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
- })
+ assert.HasNEvents(t, 3, &target)
+ requested := assert.EventIs[domain.TargetCleanupRequested](t, &target, 2)
+ assert.Equal(t, domain.TargetCleanupRequested{
+ ID: target.ID(),
+ Requested: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, requested.Requested.At())),
+ }, requested)
+ })
- t.Run("should returns the skip cleanup strategy if the configuration has failed and the target could not be updated anymore", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- target.Reconfigure()
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
- target.RequestCleanup(false, uid)
+ t.Run("should do nothing if already being cleaned up", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- s, err := target.CleanupStrategy(false)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.CleanupStrategySkip, s)
+ assert.HasNEvents(t, 3, &target)
+ })
})
- t.Run("should returns the skip cleanup strategy if the configuration has failed and has never been reachable", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ t.Run("should expose a cleanup strategy to determine how the target resources should be handled", func(t *testing.T) {
+ t.Run("should returns an error if there are running or pending deployments on the target", func(t *testing.T) {
+ target := fixture.Target()
- s, err := target.CleanupStrategy(false)
+ _, err := target.CleanupStrategy(true)
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.CleanupStrategySkip, s)
- })
+ assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
+ })
- t.Run("should returns an err if the configuration has failed but the target is still updatable", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- target.Reconfigure()
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ t.Run("should returns an error if the target is being configured", func(t *testing.T) {
+ target := fixture.Target()
- _, err := target.CleanupStrategy(false)
+ _, err := target.CleanupStrategy(false)
- testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ })
- t.Run("should returns the default strategy if the target is correctly configured", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should returns an error if the target configuration has failed and it has been at least ready once", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.Reconfigure())
+ target.Configured(target.CurrentVersion(), nil, errors.New("failed"))
- s, err := target.CleanupStrategy(false)
+ _, err := target.CleanupStrategy(false)
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.CleanupStrategyDefault, s)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
+ })
- t.Run("returns an err if trying to cleanup an app while configuring", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
+ t.Run("should returns the skip strategy if the target has never been correctly configured and is currently failing", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, errors.New("failed"))
- _, err := target.AppCleanupStrategy(false, true)
+ strategy, err := target.CleanupStrategy(false)
- testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
- })
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategySkip, strategy)
+ })
- t.Run("returns a skip strategy when trying to cleanup an app on a deleting target", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- testutil.IsNil(t, target.RequestCleanup(false, uid))
+ t.Run("should returns the default strategy if the target is ready", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- s, err := target.AppCleanupStrategy(false, false)
+ strategy, err := target.CleanupStrategy(false)
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.CleanupStrategySkip, s)
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategyDefault, strategy)
+ })
})
- t.Run("returns a skip strategy when trying to cleanup an app when no successful deployment has been made", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
+ t.Run("should expose an application cleanup strategy to determine how application resources should be handled", func(t *testing.T) {
+ t.Run("should returns the skip strategy if the target is being cleaned up", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- s, err := target.AppCleanupStrategy(false, false)
+ strategy, err := target.AppCleanupStrategy(false, true)
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.CleanupStrategySkip, s)
- })
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategySkip, strategy)
+ })
- t.Run("returns an error when trying to cleanup an app on a failed target", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- target.Reconfigure()
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ t.Run("should returns an error if there are still running deployments on the target for this application", func(t *testing.T) {
+ target := fixture.Target()
- _, err := target.AppCleanupStrategy(false, true)
+ _, err := target.AppCleanupStrategy(true, true)
- testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
- })
+ assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
+ })
- t.Run("returns an error when trying to cleanup an app but there are still running or pending deployments", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should returns the skip strategy if no successful deployment has been made and no one is running", func(t *testing.T) {
+ target := fixture.Target()
- _, err := target.AppCleanupStrategy(true, false)
+ strategy, err := target.AppCleanupStrategy(false, false)
- testutil.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
- })
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategySkip, strategy)
+ })
- t.Run("returns a default strategy when trying to remove an app and everything is good to process it", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should returns an error if the target is being configured", func(t *testing.T) {
+ target := fixture.Target()
- s, err := target.AppCleanupStrategy(false, true)
+ _, err := target.AppCleanupStrategy(false, true)
- testutil.IsNil(t, err)
- testutil.Equals(t, domain.CleanupStrategyDefault, s)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ })
- t.Run("should do nothing if trying to expose an empty entrypoints array", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
+ t.Run("should returns an error if the target configuration has failed", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, errors.New("failed"))
- target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{})
- testutil.HasNEvents(t, &target, 1)
+ _, err := target.AppCleanupStrategy(false, true)
- target.ExposeEntrypoints(app.ID(), domain.Production, nil)
- testutil.HasNEvents(t, &target, 1)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
+ })
- t.Run("should switch to the configuring state if adding new entrypoints to expose", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- appService := deployConfig.NewService("app", "")
- http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{})
- udp := appService.AddUDPEntrypoint(8080)
- dbService := deployConfig.NewService("db", "postgres:14-alpine")
- tcp := dbService.AddTCPEntrypoint(5432)
-
- services := domain.Services{appService, dbService}
-
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), services)
-
- testutil.HasNEvents(t, &target, 3)
- evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 1)
- testutil.DeepEquals(t, domain.TargetEntrypoints{
- app.ID(): {
- deployConfig.Environment(): {
- http.Name(): monad.None[domain.Port](),
- udp.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
+ t.Run("should returns the default strategy if the target is ready", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 2)
- testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status())
+ strategy, err := target.AppCleanupStrategy(false, true)
- // Should not trigger it again
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), services)
- testutil.HasNEvents(t, &target, 3)
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategyDefault, strategy)
+ })
})
- t.Run("should switch to the configuring state if adding new entrypoints to an already exposed environment", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- appService := deployConfig.NewService("app", "")
+ t.Run("could be deleted", func(t *testing.T) {
+ t.Run("should returns an error if the target has not been mark for cleanup", func(t *testing.T) {
+ target := fixture.Target()
- http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{})
+ assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, target.Delete(true))
+ })
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService})
+ t.Run("should returns an error if the target resources has not been cleaned up", func(t *testing.T) {
+ target := fixture.Target()
- testutil.HasNEvents(t, &target, 3)
- evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 1)
- testutil.DeepEquals(t, domain.TargetEntrypoints{
- app.ID(): {
- deployConfig.Environment(): {
- http.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
-
- // Adding a new entrypoint should trigger new events
- dbService := deployConfig.NewService("db", "postgres:14-alpine")
- tcp := dbService.AddTCPEntrypoint(5432)
+ assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, target.Delete(false))
+ })
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService})
-
- testutil.HasNEvents(t, &target, 5)
- evt = testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
- testutil.DeepEquals(t, domain.TargetEntrypoints{
- app.ID(): {
- deployConfig.Environment(): {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
+ t.Run("should succeed otherwise", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- // Again with the same entrypoints, should trigger nothing new
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService, deployConfig.NewService("cache", "redis:6-alpine")})
- testutil.HasNEvents(t, &target, 5)
+ assert.Nil(t, target.Delete(true))
+ assert.HasNEvents(t, 4, &target)
+ deleted := assert.EventIs[domain.TargetDeleted](t, &target, 3)
+ assert.Equal(t, domain.TargetDeleted{
+ ID: target.ID(),
+ }, deleted)
+ })
})
+}
- t.Run("should switch to the configuring state if removing entrypoints", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- appService := deployConfig.NewService("app", "")
-
- http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{})
- appService.AddUDPEntrypoint(8080)
- dbService := deployConfig.NewService("db", "postgres:14-alpine")
- tcp := dbService.AddTCPEntrypoint(5432)
-
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService})
+func Test_TargetEvents(t *testing.T) {
+ t.Run("should provide a function to check for configuration changes", func(t *testing.T) {
+ t.Run("should return false if the state is not configuring", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- // Let's remove the UDP entrypoint
- appService = deployConfig.NewService("app", "")
- appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{})
+ evt := assert.EventIs[domain.TargetStateChanged](t, &target, 1)
+ assert.False(t, evt.WentToConfiguringState())
+ })
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService})
+ t.Run("should return true if going to the configuring state", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.Reconfigure())
- testutil.HasNEvents(t, &target, 5)
- evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
- testutil.DeepEquals(t, domain.TargetEntrypoints{
- app.ID(): {
- deployConfig.Environment(): {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
+ evt := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.True(t, evt.WentToConfiguringState())
+ })
})
+}
- t.Run("should remove empty map keys when updating entrypoints", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
-
- appService := deployConfig.NewService("app", "")
+func Test_TargetEntrypointsAssigned(t *testing.T) {
+ t.Run("should provide a function to set entrypoints values", func(t *testing.T) {
+ assigned := make(domain.TargetEntrypointsAssigned)
- http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{})
- tcp := appService.AddTCPEntrypoint(5432)
+ assigned.Set("app", domain.Production, "http", 3000)
+ assigned.Set("app", domain.Production, "tcp", 3001)
+ assigned.Set("app", domain.Staging, "http", 3002)
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService})
- testutil.DeepEquals(t, domain.TargetEntrypoints{
- app.ID(): {
+ assert.DeepEqual(t, domain.TargetEntrypointsAssigned{
+ "app": {
domain.Production: {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
+ "http": 3000,
+ "tcp": 3001,
+ },
+ domain.Staging: {
+ "http": 3002,
},
},
- }, target.CustomEntrypoints())
-
- target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{})
-
- testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
- })
-
- t.Run("should not be removed if no cleanup request has been set", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
-
- err := target.Delete(true)
-
- testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err)
- })
-
- t.Run("should not be removed if target resources have not been cleaned up", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- testutil.IsNil(t, target.RequestCleanup(false, uid)) // No application is using it
-
- err := target.Delete(false)
-
- testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err)
- })
-
- t.Run("could be removed if resources have been cleaned up", func(t *testing.T) {
- target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid))
- target.Configured(target.CurrentVersion(), nil, nil)
- testutil.IsNil(t, target.RequestCleanup(false, uid))
-
- err := target.Delete(true)
-
- testutil.IsNil(t, err)
- testutil.EventIs[domain.TargetDeleted](t, &target, 3)
- })
-}
-
-func Test_TargetEvents(t *testing.T) {
- t.Run("TargetStateChanged should provide a function to check for configuration changes", func(t *testing.T) {
- target := must.Panic(domain.NewTarget("my-target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://my-url.com")), true),
- domain.NewProviderConfigRequirement(dummyProviderConfig{}, true), "uid",
- ))
- target.Configured(target.CurrentVersion(), nil, nil)
-
- evt := testutil.EventIs[domain.TargetStateChanged](t, &target, 1)
- testutil.IsFalse(t, evt.WentToConfiguringState())
-
- testutil.IsNil(t, target.Reconfigure())
-
- evt = testutil.EventIs[domain.TargetStateChanged](t, &target, 2)
- testutil.IsTrue(t, evt.WentToConfiguringState())
+ }, assigned)
})
}
-
-type dummyProviderConfig struct {
- data string
- fingerprint string
-}
-
-func (d dummyProviderConfig) Kind() string { return "dummy" }
-func (d dummyProviderConfig) Fingerprint() string { return d.fingerprint }
-func (d dummyProviderConfig) String() string { return d.fingerprint }
-
-func (d dummyProviderConfig) Equals(other domain.ProviderConfig) bool {
- return d == other
-}
diff --git a/internal/deployment/domain/url_test.go b/internal/deployment/domain/url_test.go
index b3d7fe7b..f69bcb9a 100644
--- a/internal/deployment/domain/url_test.go
+++ b/internal/deployment/domain/url_test.go
@@ -4,8 +4,8 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Url(t *testing.T) {
@@ -26,37 +26,37 @@ func Test_Url(t *testing.T) {
u, err := domain.UrlFrom(test.value)
if test.valid {
- testutil.IsNil(t, err)
- testutil.Equals(t, test.value, u.String())
+ assert.Nil(t, err)
+ assert.Equal(t, test.value, u.String())
} else {
- testutil.ErrorIs(t, domain.ErrInvalidUrl, err)
+ assert.ErrorIs(t, domain.ErrInvalidUrl, err)
}
})
}
})
t.Run("should get wether its a secure url or not", func(t *testing.T) {
- httpUrl, _ := domain.UrlFrom("http://something.com")
- httpsUrl, _ := domain.UrlFrom("https://something.com")
+ httpUrl := must.Panic(domain.UrlFrom("http://something.com"))
+ httpsUrl := must.Panic(domain.UrlFrom("https://something.com"))
- testutil.IsFalse(t, httpUrl.UseSSL())
- testutil.IsTrue(t, httpsUrl.UseSSL())
+ assert.False(t, httpUrl.UseSSL())
+ assert.True(t, httpsUrl.UseSSL())
})
t.Run("should be able to prepend a subdomain", func(t *testing.T) {
- url, _ := domain.UrlFrom("http://something.com")
+ url := must.Panic(domain.UrlFrom("http://something.com"))
subdomained := url.SubDomain("an-app")
- testutil.Equals(t, "http://something.com", url.String())
- testutil.Equals(t, "http://an-app.something.com", subdomained.String())
+ assert.Equal(t, "http://something.com", url.String())
+ assert.Equal(t, "http://an-app.something.com", subdomained.String())
})
t.Run("should implement the valuer interface", func(t *testing.T) {
- url, _ := domain.UrlFrom("http://something.com")
+ url := must.Panic(domain.UrlFrom("http://something.com"))
value, err := url.Value()
- testutil.IsNil(t, err)
- testutil.Equals(t, "http://something.com", value.(string))
+ assert.Nil(t, err)
+ assert.Equal(t, "http://something.com", value.(string))
})
t.Run("should implement the scanner interface", func(t *testing.T) {
@@ -66,16 +66,16 @@ func Test_Url(t *testing.T) {
)
err := url.Scan(value)
- testutil.IsNil(t, err)
- testutil.Equals(t, "http://something.com", url.String())
+ assert.Nil(t, err)
+ assert.Equal(t, "http://something.com", url.String())
})
t.Run("should marshal to json", func(t *testing.T) {
- url, _ := domain.UrlFrom("http://something.com")
+ url := must.Panic(domain.UrlFrom("http://something.com"))
json, err := url.MarshalJSON()
- testutil.IsNil(t, err)
- testutil.Equals(t, `"http://something.com"`, string(json))
+ assert.Nil(t, err)
+ assert.Equal(t, `"http://something.com"`, string(json))
})
t.Run("should unmarshal from json", func(t *testing.T) {
@@ -85,36 +85,36 @@ func Test_Url(t *testing.T) {
)
err := url.UnmarshalJSON([]byte(value))
- testutil.IsNil(t, err)
- testutil.Equals(t, "http://something.com", url.String())
+ assert.Nil(t, err)
+ assert.Equal(t, "http://something.com", url.String())
})
t.Run("should retrieve the user part of an url if any", func(t *testing.T) {
url := must.Panic(domain.UrlFrom("http://seelf@docker.localhost"))
- testutil.IsTrue(t, url.User().HasValue())
- testutil.Equals(t, "seelf", url.User().MustGet())
+ assert.True(t, url.User().HasValue())
+ assert.Equal(t, "seelf", url.User().MustGet())
url = must.Panic(domain.UrlFrom("http://docker.localhost"))
- testutil.IsFalse(t, url.User().HasValue())
+ assert.False(t, url.User().HasValue())
})
t.Run("should be able to remove the user part of an url", func(t *testing.T) {
url := must.Panic(domain.UrlFrom("http://seelf@docker.localhost"))
- testutil.Equals(t, "http://docker.localhost", url.WithoutUser().String())
- testutil.Equals(t, "http://seelf@docker.localhost", url.String())
+ assert.Equal(t, "http://docker.localhost", url.WithoutUser().String())
+ assert.Equal(t, "http://seelf@docker.localhost", url.String())
url = must.Panic(domain.UrlFrom("http://docker.localhost"))
- testutil.Equals(t, "http://docker.localhost", url.WithoutUser().String())
+ assert.Equal(t, "http://docker.localhost", url.WithoutUser().String())
})
t.Run("should be able to remove path and query from an url", func(t *testing.T) {
url := must.Panic(domain.UrlFrom("http://docker.localhost/some/path?query=value"))
- testutil.Equals(t, "http://docker.localhost", url.Root().String())
- testutil.Equals(t, "http://docker.localhost/some/path?query=value", url.String())
+ assert.Equal(t, "http://docker.localhost", url.Root().String())
+ assert.Equal(t, "http://docker.localhost/some/path?query=value", url.String())
})
}
diff --git a/internal/deployment/domain/version_control_test.go b/internal/deployment/domain/version_control_test.go
index 659c0deb..9fc77b61 100644
--- a/internal/deployment/domain/version_control_test.go
+++ b/internal/deployment/domain/version_control_test.go
@@ -1,11 +1,10 @@
package domain_test
import (
- "fmt"
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_VersionControl(t *testing.T) {
@@ -14,8 +13,8 @@ func Test_VersionControl(t *testing.T) {
conf := domain.NewVersionControl(url)
- testutil.Equals(t, url, conf.Url())
- testutil.IsFalse(t, conf.Token().HasValue())
+ assert.Equal(t, url, conf.Url())
+ assert.False(t, conf.Token().HasValue())
})
t.Run("should hold a token if authentication is needed", func(t *testing.T) {
@@ -27,8 +26,8 @@ func Test_VersionControl(t *testing.T) {
conf := domain.NewVersionControl(url)
conf.Authenticated(token)
- testutil.Equals(t, url, conf.Url())
- testutil.Equals(t, token, conf.Token().Get(""))
+ assert.Equal(t, url, conf.Url())
+ assert.Equal(t, token, conf.Token().Get(""))
})
t.Run("could update the url", func(t *testing.T) {
@@ -42,8 +41,8 @@ func Test_VersionControl(t *testing.T) {
conf.Authenticated(token)
conf.HasUrl(newUrl)
- testutil.Equals(t, newUrl, conf.Url())
- testutil.Equals(t, token, conf.Token().Get(""))
+ assert.Equal(t, newUrl, conf.Url())
+ assert.Equal(t, token, conf.Token().Get(""))
})
t.Run("could remove a token", func(t *testing.T) {
@@ -53,87 +52,7 @@ func Test_VersionControl(t *testing.T) {
conf.Authenticated("a token")
conf.Public()
- testutil.Equals(t, url, conf.Url())
- testutil.IsFalse(t, conf.Token().HasValue())
- })
-
- t.Run("should be able to compare itself with another config", func(t *testing.T) {
- var (
- url, _ = domain.UrlFrom("http://somewhere.git")
- sameUrlDifferentStruct, _ = domain.UrlFrom("http://somewhere.git")
- anotherUrl, _ = domain.UrlFrom("http://somewhere-else.git")
- token string = "some token"
- anotherToken string = "another token"
- )
-
- tests := []struct {
- first func() domain.VersionControl
- second func() domain.VersionControl
- expected bool
- }{
- {
- func() domain.VersionControl {
- conf := domain.NewVersionControl(url)
- conf.Authenticated(token)
- return conf
- },
- func() domain.VersionControl {
- return domain.NewVersionControl(sameUrlDifferentStruct)
- },
- false,
- },
- {
- func() domain.VersionControl {
- return domain.NewVersionControl(url)
- },
- func() domain.VersionControl {
- return domain.NewVersionControl(anotherUrl)
- },
- false,
- },
- {
- func() domain.VersionControl {
- conf := domain.NewVersionControl(url)
- conf.Authenticated(token)
- return conf
- },
- func() domain.VersionControl {
- conf := domain.NewVersionControl(sameUrlDifferentStruct)
- conf.Authenticated(anotherToken)
- return conf
- },
- false,
- },
- {
- func() domain.VersionControl {
- return domain.NewVersionControl(url)
- },
- func() domain.VersionControl {
- return domain.NewVersionControl(sameUrlDifferentStruct)
- },
- true,
- },
- {
- func() domain.VersionControl {
- conf := domain.NewVersionControl(url)
- conf.Authenticated(token)
- return conf
- },
- func() domain.VersionControl {
- conf := domain.NewVersionControl(sameUrlDifferentStruct)
- conf.Authenticated(token)
- return conf
- },
- true,
- },
- }
-
- for _, tt := range tests {
- f := tt.first()
- s := tt.second()
- t.Run(fmt.Sprintf("%v %v", f, s), func(t *testing.T) {
- testutil.Equals(t, tt.expected, f == s)
- })
- }
+ assert.Equal(t, url, conf.Url())
+ assert.False(t, conf.Token().HasValue())
})
}
diff --git a/internal/deployment/fixture/app.go b/internal/deployment/fixture/app.go
new file mode 100644
index 00000000..965f6dd2
--- /dev/null
+++ b/internal/deployment/fixture/app.go
@@ -0,0 +1,65 @@
+//go:build !release
+
+package fixture
+
+import (
+ auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/pkg/id"
+ "github.com/YuukanOO/seelf/pkg/must"
+)
+
+type (
+ appOption struct {
+ name domain.AppName
+ production domain.EnvironmentConfig
+ staging domain.EnvironmentConfig
+ createdBy auth.UserID
+ }
+
+ AppOptionBuilder func(*appOption)
+)
+
+func App(options ...AppOptionBuilder) domain.App {
+ opts := appOption{
+ name: id.New[domain.AppName](),
+ production: domain.NewEnvironmentConfig(id.New[domain.TargetID]()),
+ staging: domain.NewEnvironmentConfig(id.New[domain.TargetID]()),
+ createdBy: id.New[auth.UserID](),
+ }
+
+ for _, o := range options {
+ o(&opts)
+ }
+
+ return must.Panic(domain.NewApp(opts.name,
+ domain.NewEnvironmentConfigRequirement(opts.production, true, true),
+ domain.NewEnvironmentConfigRequirement(opts.staging, true, true),
+ opts.createdBy,
+ ))
+}
+
+func WithAppName(name domain.AppName) AppOptionBuilder {
+ return func(o *appOption) {
+ o.name = name
+ }
+}
+
+func WithAppCreatedBy(uid auth.UserID) AppOptionBuilder {
+ return func(o *appOption) {
+ o.createdBy = uid
+ }
+}
+
+func WithProductionConfig(production domain.EnvironmentConfig) AppOptionBuilder {
+ return func(o *appOption) {
+ o.production = production
+ }
+}
+
+func WithEnvironmentConfig(production, staging domain.EnvironmentConfig) AppOptionBuilder {
+ return func(o *appOption) {
+ o.production = production
+ o.staging = staging
+ }
+}
diff --git a/internal/deployment/fixture/app_test.go b/internal/deployment/fixture/app_test.go
new file mode 100644
index 00000000..399b82b9
--- /dev/null
+++ b/internal/deployment/fixture/app_test.go
@@ -0,0 +1,50 @@
+package fixture_test
+
+import (
+ "testing"
+
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+)
+
+func Test_App(t *testing.T) {
+ t.Run("should be able to build a random app", func(t *testing.T) {
+ app := fixture.App()
+
+ assert.NotZero(t, app.ID())
+ })
+
+ t.Run("should be able to build an app with a given name", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppName("foo"))
+
+ created := assert.EventIs[domain.AppCreated](t, &app, 0)
+ assert.Equal(t, "foo", created.Name)
+ })
+
+ t.Run("should be able to build an app created by a specific user id", func(t *testing.T) {
+ app := fixture.App(fixture.WithAppCreatedBy("uid"))
+
+ created := assert.EventIs[domain.AppCreated](t, &app, 0)
+ assert.Equal(t, "uid", created.Created.By())
+ })
+
+ t.Run("should be able to build an app with given production and staging configuration", func(t *testing.T) {
+ production := domain.NewEnvironmentConfig("production_id")
+ staging := domain.NewEnvironmentConfig("staging_id")
+ app := fixture.App(fixture.WithEnvironmentConfig(production, staging))
+
+ created := assert.EventIs[domain.AppCreated](t, &app, 0)
+ assert.DeepEqual(t, production, created.Production)
+ assert.DeepEqual(t, staging, created.Staging)
+ })
+
+ t.Run("should be able to build an app with given production configuration", func(t *testing.T) {
+ config := domain.NewEnvironmentConfig("production_id")
+ app := fixture.App(fixture.WithProductionConfig(config))
+
+ created := assert.EventIs[domain.AppCreated](t, &app, 0)
+ assert.DeepEqual(t, config, created.Production)
+ assert.NotEqual(t, config.Target(), created.Staging.Target())
+ })
+}
diff --git a/internal/deployment/fixture/database.go b/internal/deployment/fixture/database.go
new file mode 100644
index 00000000..0662fe42
--- /dev/null
+++ b/internal/deployment/fixture/database.go
@@ -0,0 +1,142 @@
+//go:build !release
+
+package fixture
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/YuukanOO/seelf/cmd/config"
+ auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authsqlite "github.com/YuukanOO/seelf/internal/auth/infra/sqlite"
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ deployment "github.com/YuukanOO/seelf/internal/deployment/infra/sqlite"
+ "github.com/YuukanOO/seelf/pkg/bus/spy"
+ scheduler "github.com/YuukanOO/seelf/pkg/bus/sqlite"
+ "github.com/YuukanOO/seelf/pkg/log"
+ "github.com/YuukanOO/seelf/pkg/must"
+ "github.com/YuukanOO/seelf/pkg/ostools"
+ "github.com/YuukanOO/seelf/pkg/storage/sqlite"
+)
+
+type (
+ seed struct {
+ users []*auth.User
+ targets []*domain.Target
+ apps []*domain.App
+ deployments []*domain.Deployment
+ registries []*domain.Registry
+ }
+
+ Context struct {
+ Config config.Configuration
+ Context context.Context // If users has been seeded, will be authenticated as the first one
+ Dispatcher spy.Dispatcher
+ TargetsStore deployment.TargetsStore
+ AppsStore deployment.AppsStore
+ DeploymentsStore deployment.DeploymentsStore
+ RegistriesStore deployment.RegistriesStore
+ }
+
+ SeedBuilder func(*seed)
+)
+
+func PrepareDatabase(t testing.TB, options ...SeedBuilder) *Context {
+ result := Context{
+ Config: config.Default(config.WithTestDefaults()),
+ Context: context.Background(),
+ Dispatcher: spy.NewDispatcher(),
+ }
+
+ if err := ostools.MkdirAll(result.Config.DataDir()); err != nil {
+ t.Fatal(err)
+ }
+
+ db, err := sqlite.Open(result.Config.ConnectionString(), must.Panic(log.NewLogger()), result.Dispatcher)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Cleanup(func() {
+ db.Close()
+ os.RemoveAll(result.Config.DataDir())
+ })
+
+ // FIXME: scheduler migrations are needed because some migrations may queue a job by inserting inside
+ // the scheduled_jobs table. That's a mistake from my side and I should fix it later.
+ if err = db.Migrate(scheduler.Migrations, authsqlite.Migrations, deployment.Migrations); err != nil {
+ t.Fatal(err)
+ }
+
+ result.AppsStore = deployment.NewAppsStore(db)
+ result.TargetsStore = deployment.NewTargetsStore(db)
+ result.DeploymentsStore = deployment.NewDeploymentsStore(db)
+ result.RegistriesStore = deployment.NewRegistriesStore(db)
+
+ // Seed the database
+ var s seed
+
+ for _, o := range options {
+ o(&s)
+ }
+
+ if len(s.users) > 0 {
+ if err := authsqlite.NewUsersStore(db).Write(result.Context, s.users...); err != nil {
+ t.Fatal(err)
+ }
+ result.Context = auth.WithUserID(result.Context, s.users[0].ID()) // The first created user will be used as the authenticated one
+ }
+
+ if err := result.RegistriesStore.Write(result.Context, s.registries...); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := result.TargetsStore.Write(result.Context, s.targets...); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := result.AppsStore.Write(result.Context, s.apps...); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := result.DeploymentsStore.Write(result.Context, s.deployments...); err != nil {
+ t.Fatal(err)
+ }
+
+ // Reset the dispatcher after seeding
+ result.Dispatcher.Reset()
+
+ return &result
+}
+
+func WithUsers(users ...*auth.User) SeedBuilder {
+ return func(s *seed) {
+ s.users = users
+ }
+}
+
+func WithTargets(targets ...*domain.Target) SeedBuilder {
+ return func(s *seed) {
+ s.targets = targets
+ }
+}
+
+func WithApps(apps ...*domain.App) SeedBuilder {
+ return func(s *seed) {
+ s.apps = apps
+ }
+}
+
+func WithDeployments(deployments ...*domain.Deployment) SeedBuilder {
+ return func(s *seed) {
+ s.deployments = deployments
+ }
+}
+
+func WithRegistries(registries ...*domain.Registry) SeedBuilder {
+ return func(s *seed) {
+ s.registries = registries
+ }
+}
diff --git a/internal/deployment/fixture/database_test.go b/internal/deployment/fixture/database_test.go
new file mode 100644
index 00000000..7879c3ee
--- /dev/null
+++ b/internal/deployment/fixture/database_test.go
@@ -0,0 +1,53 @@
+package fixture_test
+
+import (
+ "testing"
+
+ auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ authfixture "github.com/YuukanOO/seelf/internal/auth/fixture"
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+)
+
+func Test_Database(t *testing.T) {
+ t.Run("should be able to prepare a database without seeding it", func(t *testing.T) {
+ ctx := fixture.PrepareDatabase(t)
+
+ assert.NotNil(t, ctx)
+ assert.NotNil(t, ctx.Config)
+ assert.NotNil(t, ctx.AppsStore)
+ assert.NotNil(t, ctx.TargetsStore)
+ assert.NotNil(t, ctx.AppsStore)
+ assert.NotNil(t, ctx.DeploymentsStore)
+ assert.NotNil(t, ctx.RegistriesStore)
+ assert.NotNil(t, ctx.Dispatcher)
+ assert.HasLength(t, 0, ctx.Dispatcher.Signals())
+ assert.HasLength(t, 0, ctx.Dispatcher.Requests())
+ })
+
+ t.Run("should seed correctly and attach the first user id to the created context", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ config := domain.NewEnvironmentConfig(target.ID())
+ app := fixture.App(
+ fixture.WithEnvironmentConfig(config, config),
+ fixture.WithAppCreatedBy(user.ID()),
+ )
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID()))
+ deployment := fixture.Deployment(
+ fixture.FromApp(app),
+ fixture.WithDeploymentRequestedBy(user.ID()),
+ )
+
+ ctx := fixture.PrepareDatabase(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ fixture.WithApps(&app),
+ fixture.WithRegistries(®istry),
+ fixture.WithDeployments(&deployment),
+ )
+
+ assert.Equal(t, user.ID(), auth.CurrentUser(ctx.Context).Get(""))
+ })
+}
diff --git a/internal/deployment/fixture/deployment.go b/internal/deployment/fixture/deployment.go
new file mode 100644
index 00000000..581f0140
--- /dev/null
+++ b/internal/deployment/fixture/deployment.go
@@ -0,0 +1,97 @@
+//go:build !release
+
+package fixture
+
+import (
+ "database/sql/driver"
+
+ auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/pkg/id"
+ "github.com/YuukanOO/seelf/pkg/must"
+ "github.com/YuukanOO/seelf/pkg/storage"
+)
+
+type (
+ deploymentOption struct {
+ uid auth.UserID
+ environment domain.Environment
+ source domain.SourceData
+ app domain.App
+ }
+
+ DeploymentOptionBuilder func(*deploymentOption)
+)
+
+func Deployment(options ...DeploymentOptionBuilder) domain.Deployment {
+ opts := deploymentOption{
+ uid: id.New[auth.UserID](),
+ environment: domain.Production,
+ source: SourceData(),
+ app: App(),
+ }
+
+ for _, o := range options {
+ o(&opts)
+ }
+
+ return must.Panic(opts.app.NewDeployment(1, opts.source, opts.environment, opts.uid))
+}
+
+func FromApp(app domain.App) DeploymentOptionBuilder {
+ return func(o *deploymentOption) {
+ o.app = app
+ }
+}
+
+func WithSourceData(source domain.SourceData) DeploymentOptionBuilder {
+ return func(o *deploymentOption) {
+ o.source = source
+ }
+}
+
+func WithDeploymentRequestedBy(uid auth.UserID) DeploymentOptionBuilder {
+ return func(o *deploymentOption) {
+ o.uid = uid
+ }
+}
+
+func ForEnvironment(environment domain.Environment) DeploymentOptionBuilder {
+ return func(o *deploymentOption) {
+ o.environment = environment
+ }
+}
+
+type (
+ sourceDataOption struct {
+ UseVersionControl bool
+ }
+
+ SourceDataOptionBuilder func(*sourceDataOption)
+)
+
+func SourceData(options ...SourceDataOptionBuilder) domain.SourceData {
+ var opts sourceDataOption
+
+ for _, o := range options {
+ o(&opts)
+ }
+
+ return opts
+}
+
+func (sourceDataOption) Kind() string { return "test" }
+func (m sourceDataOption) NeedVersionControl() bool { return m.UseVersionControl }
+func (m sourceDataOption) Value() (driver.Value, error) { return storage.ValueJSON(m) }
+
+func WithVersionControlNeeded() SourceDataOptionBuilder {
+ return func(o *sourceDataOption) {
+ o.UseVersionControl = true
+ }
+}
+
+func init() {
+ domain.SourceDataTypes.Register(sourceDataOption{}, func(s string) (domain.SourceData, error) {
+ return storage.UnmarshalJSON[sourceDataOption](s)
+ })
+}
diff --git a/internal/deployment/fixture/deployment_test.go b/internal/deployment/fixture/deployment_test.go
new file mode 100644
index 00000000..fa0ef58e
--- /dev/null
+++ b/internal/deployment/fixture/deployment_test.go
@@ -0,0 +1,63 @@
+package fixture_test
+
+import (
+ "testing"
+
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+)
+
+func Test_Deployment(t *testing.T) {
+ t.Run("should be able to create a new deployment", func(t *testing.T) {
+ deployment := fixture.Deployment()
+
+ assert.NotZero(t, deployment.ID())
+ assert.Equal(t, domain.Production, deployment.Config().Environment())
+ })
+
+ t.Run("should be able to create a new deployment from a given app", func(t *testing.T) {
+ app := fixture.App()
+ deployment := fixture.Deployment(fixture.FromApp(app))
+
+ created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0)
+ assert.Equal(t, app.ID(), created.ID.AppID())
+ })
+
+ t.Run("should be able to create a new deployment requested by a given user id", func(t *testing.T) {
+ deployment := fixture.Deployment(fixture.WithDeploymentRequestedBy("uid"))
+
+ created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0)
+ assert.Equal(t, "uid", created.Requested.By())
+ })
+
+ t.Run("should be able to create a new deployment with a given source data", func(t *testing.T) {
+ source := fixture.SourceData()
+ deployment := fixture.Deployment(fixture.WithSourceData(source))
+
+ created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0)
+ assert.Equal(t, source, created.Source)
+ })
+
+ t.Run("should be able to create a new deployment with a given environment", func(t *testing.T) {
+ deployment := fixture.Deployment(fixture.ForEnvironment(domain.Staging))
+
+ created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0)
+ assert.Equal(t, domain.Staging, created.Config.Environment())
+ })
+}
+
+func Test_SourceData(t *testing.T) {
+ t.Run("should be able to create a source data", func(t *testing.T) {
+ source := fixture.SourceData()
+
+ assert.Equal(t, "test", source.Kind())
+ assert.False(t, source.NeedVersionControl())
+ })
+
+ t.Run("should be able to create a source data with version control needed", func(t *testing.T) {
+ source := fixture.SourceData(fixture.WithVersionControlNeeded())
+
+ assert.True(t, source.NeedVersionControl())
+ })
+}
diff --git a/internal/deployment/fixture/registry.go b/internal/deployment/fixture/registry.go
new file mode 100644
index 00000000..c67ce576
--- /dev/null
+++ b/internal/deployment/fixture/registry.go
@@ -0,0 +1,52 @@
+//go:build !release
+
+package fixture
+
+import (
+ auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/pkg/id"
+ "github.com/YuukanOO/seelf/pkg/must"
+)
+
+type (
+ registryOption struct {
+ name string
+ url domain.Url
+ uid auth.UserID
+ }
+
+ RegistryOptionBuilder func(*registryOption)
+)
+
+func Registry(options ...RegistryOptionBuilder) domain.Registry {
+ opts := registryOption{
+ name: id.New[string](),
+ url: must.Panic(domain.UrlFrom("http://" + id.New[string]() + ".com")),
+ uid: id.New[auth.UserID](),
+ }
+
+ for _, o := range options {
+ o(&opts)
+ }
+
+ return must.Panic(domain.NewRegistry(opts.name, domain.NewRegistryUrlRequirement(opts.url, true), opts.uid))
+}
+
+func WithRegistryName(name string) RegistryOptionBuilder {
+ return func(o *registryOption) {
+ o.name = name
+ }
+}
+
+func WithRegistryCreatedBy(uid auth.UserID) RegistryOptionBuilder {
+ return func(o *registryOption) {
+ o.uid = uid
+ }
+}
+
+func WithUrl(url domain.Url) RegistryOptionBuilder {
+ return func(o *registryOption) {
+ o.url = url
+ }
+}
diff --git a/internal/deployment/fixture/registry_test.go b/internal/deployment/fixture/registry_test.go
new file mode 100644
index 00000000..2bf497c4
--- /dev/null
+++ b/internal/deployment/fixture/registry_test.go
@@ -0,0 +1,40 @@
+package fixture_test
+
+import (
+ "testing"
+
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+ "github.com/YuukanOO/seelf/pkg/must"
+)
+
+func Test_Registry(t *testing.T) {
+ t.Run("should be able to create a random registry", func(t *testing.T) {
+ registry := fixture.Registry()
+
+ assert.NotZero(t, registry.ID())
+ })
+
+ t.Run("should be able to create a registry with a given name", func(t *testing.T) {
+ registry := fixture.Registry(fixture.WithRegistryName("my-registry"))
+
+ created := assert.EventIs[domain.RegistryCreated](t, ®istry, 0)
+ assert.Equal(t, "my-registry", created.Name)
+ })
+
+ t.Run("should be able to create a registry created by a given user id", func(t *testing.T) {
+ registry := fixture.Registry(fixture.WithRegistryCreatedBy("uid"))
+
+ created := assert.EventIs[domain.RegistryCreated](t, ®istry, 0)
+ assert.Equal(t, "uid", created.Created.By())
+ })
+
+ t.Run("should be able to create a registry with a given url", func(t *testing.T) {
+ url := must.Panic(domain.UrlFrom("https://my-registry.com"))
+ registry := fixture.Registry(fixture.WithUrl(url))
+
+ created := assert.EventIs[domain.RegistryCreated](t, ®istry, 0)
+ assert.Equal(t, url, created.Url)
+ })
+}
diff --git a/internal/deployment/fixture/target.go b/internal/deployment/fixture/target.go
new file mode 100644
index 00000000..fb3076b8
--- /dev/null
+++ b/internal/deployment/fixture/target.go
@@ -0,0 +1,119 @@
+//go:build !release
+
+package fixture
+
+import (
+ "database/sql/driver"
+
+ auth "github.com/YuukanOO/seelf/internal/auth/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/pkg/id"
+ "github.com/YuukanOO/seelf/pkg/must"
+ "github.com/YuukanOO/seelf/pkg/storage"
+)
+
+type (
+ targetOption struct {
+ name string
+ provider domain.ProviderConfig
+ uid auth.UserID
+ }
+
+ TargetOptionBuilder func(*targetOption)
+)
+
+func Target(options ...TargetOptionBuilder) domain.Target {
+ opts := targetOption{
+ name: id.New[string](),
+ provider: ProviderConfig(),
+ uid: id.New[auth.UserID](),
+ }
+
+ for _, o := range options {
+ o(&opts)
+ }
+
+ return must.Panic(domain.NewTarget(opts.name,
+ domain.NewProviderConfigRequirement(opts.provider, true),
+ opts.uid))
+}
+
+func WithTargetName(name string) TargetOptionBuilder {
+ return func(opts *targetOption) {
+ opts.name = name
+ }
+}
+
+func WithTargetCreatedBy(uid auth.UserID) TargetOptionBuilder {
+ return func(opts *targetOption) {
+ opts.uid = uid
+ }
+}
+
+func WithProviderConfig(config domain.ProviderConfig) TargetOptionBuilder {
+ return func(opts *targetOption) {
+ opts.provider = config
+ }
+}
+
+type (
+ providerConfig struct {
+ Kind_ string
+ Data string
+ Fingerprint_ string
+ }
+
+ ProviderConfigBuilder func(*providerConfig)
+)
+
+func ProviderConfig(options ...ProviderConfigBuilder) (result domain.ProviderConfig) {
+ config := providerConfig{
+ Data: id.New[string](),
+ Kind_: id.New[string](),
+ Fingerprint_: id.New[string](),
+ }
+
+ for _, o := range options {
+ o(&config)
+ }
+
+ result = config
+
+ // Just ignore the panic due to the multiple registration of same kind
+ defer func() {
+ _ = recover()
+ }()
+
+ domain.ProviderConfigTypes.Register(config, func(s string) (domain.ProviderConfig, error) {
+ return storage.UnmarshalJSON[providerConfig](s)
+ })
+
+ return
+}
+
+func WithFingerprint(fingerprint string) ProviderConfigBuilder {
+ return func(config *providerConfig) {
+ config.Fingerprint_ = fingerprint
+ }
+}
+
+func WithKind(kind string) ProviderConfigBuilder {
+ return func(config *providerConfig) {
+ config.Kind_ = kind
+ }
+}
+
+func WithData(data string) ProviderConfigBuilder {
+ return func(config *providerConfig) {
+ config.Data = data
+ }
+}
+
+func (d providerConfig) Kind() string { return d.Kind_ }
+func (d providerConfig) Fingerprint() string { return d.Fingerprint_ }
+func (d providerConfig) String() string { return d.Fingerprint_ }
+func (d providerConfig) Value() (driver.Value, error) { return storage.ValueJSON(d) }
+
+func (d providerConfig) Equals(other domain.ProviderConfig) bool {
+ return d == other
+}
diff --git a/internal/deployment/fixture/target_test.go b/internal/deployment/fixture/target_test.go
new file mode 100644
index 00000000..32ddba7c
--- /dev/null
+++ b/internal/deployment/fixture/target_test.go
@@ -0,0 +1,76 @@
+package fixture_test
+
+import (
+ "testing"
+
+ "github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
+ "github.com/YuukanOO/seelf/pkg/assert"
+)
+
+func Test_Target(t *testing.T) {
+ t.Run("should be able to create a target", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.NotZero(t, target.ID())
+ })
+
+ t.Run("should be able to create a target with a given name", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetName("name"))
+
+ created := assert.EventIs[domain.TargetCreated](t, &target, 0)
+ assert.Equal(t, "name", created.Name)
+ })
+
+ t.Run("should be able to create a target with a given user id", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetCreatedBy("id"))
+
+ created := assert.EventIs[domain.TargetCreated](t, &target, 0)
+ assert.Equal(t, "id", created.Created.By())
+ })
+
+ t.Run("should be able to create a target with a given provider config", func(t *testing.T) {
+ config := fixture.ProviderConfig()
+ target := fixture.Target(fixture.WithProviderConfig(config))
+
+ created := assert.EventIs[domain.TargetCreated](t, &target, 0)
+ assert.DeepEqual(t, config, created.Provider)
+ })
+}
+
+func Test_ProviderConfig(t *testing.T) {
+ t.Run("should be able to create a provider config", func(t *testing.T) {
+ config := fixture.ProviderConfig()
+
+ assert.NotZero(t, config.Fingerprint())
+ assert.NotZero(t, config.Kind())
+ })
+
+ t.Run("should be able to create a provider config with a given fingerprint", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithFingerprint("fingerprint"))
+
+ assert.Equal(t, "fingerprint", config.Fingerprint())
+ })
+
+ t.Run("should be able to create a provider config with a given kind", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("kind"))
+
+ assert.Equal(t, "kind", config.Kind())
+ })
+
+ t.Run("should be able to create a provider config with a given data", func(t *testing.T) {
+ one := fixture.ProviderConfig(
+ fixture.WithKind("kind"),
+ fixture.WithFingerprint("fingerprint"),
+ fixture.WithData("data"))
+ two := fixture.ProviderConfig(
+ fixture.WithKind("kind"),
+ fixture.WithFingerprint("fingerprint"),
+ fixture.WithData("data"))
+ three := fixture.ProviderConfig()
+
+ assert.True(t, one.Equals(two))
+ assert.False(t, one.Equals(three))
+ assert.False(t, two.Equals(three))
+ })
+}
diff --git a/internal/deployment/infra/artifact/local_artifact_manager.go b/internal/deployment/infra/artifact/local_artifact_manager.go
index fa88c12b..a5711d35 100644
--- a/internal/deployment/infra/artifact/local_artifact_manager.go
+++ b/internal/deployment/infra/artifact/local_artifact_manager.go
@@ -55,16 +55,16 @@ func NewLocal(options LocalOptions, logger log.Logger) domain.ArtifactManager {
func (a *localArtifactManager) PrepareBuild(
ctx context.Context,
- depl domain.Deployment,
+ deployment domain.Deployment,
) (domain.DeploymentContext, error) {
- logfile, err := ostools.OpenAppend(a.LogPath(ctx, depl))
+ logFile, err := ostools.OpenAppend(a.LogPath(ctx, deployment))
if err != nil {
a.logger.Error(err)
return domain.DeploymentContext{}, ErrArtifactOpenLoggerFailed
}
- logger := newLogger(logfile)
+ logger := newLogger(logFile)
defer func() {
if err == nil {
@@ -76,7 +76,7 @@ func (a *localArtifactManager) PrepareBuild(
logger.Close() // And close the logger right now
}()
- buildDirectory, err := a.deploymentPath(depl)
+ buildDirectory, err := a.deploymentPath(deployment)
if err != nil {
return domain.DeploymentContext{}, err
@@ -121,15 +121,15 @@ func (a *localArtifactManager) appPath(appID domain.AppID) string {
return filepath.Join(a.appsDirectory, string(appID))
}
-func (a *localArtifactManager) deploymentPath(depl domain.Deployment) (string, error) {
+func (a *localArtifactManager) deploymentPath(deployment domain.Deployment) (string, error) {
var w strings.Builder
if err := a.options.DeploymentDirTemplate().Execute(&w, deploymentTemplateData{
- Number: depl.ID().DeploymentNumber(),
- Environment: depl.Config().Environment(),
+ Number: deployment.ID().DeploymentNumber(),
+ Environment: deployment.Config().Environment(),
}); err != nil {
return "", err
}
- return filepath.Join(a.appPath(depl.ID().AppID()), w.String()), nil
+ return filepath.Join(a.appPath(deployment.ID().AppID()), w.String()), nil
}
diff --git a/internal/deployment/infra/artifact/local_artifact_manager_test.go b/internal/deployment/infra/artifact/local_artifact_manager_test.go
index df9c1137..ec9bb00a 100644
--- a/internal/deployment/infra/artifact/local_artifact_manager_test.go
+++ b/internal/deployment/infra/artifact/local_artifact_manager_test.go
@@ -9,9 +9,9 @@ import (
"github.com/YuukanOO/seelf/internal/deployment/domain"
"github.com/YuukanOO/seelf/internal/deployment/infra/artifact"
"github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/log"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_LocalArtifactManager(t *testing.T) {
@@ -34,27 +34,27 @@ func Test_LocalArtifactManager(t *testing.T) {
manager := sut()
ctx, err := manager.PrepareBuild(context.Background(), depl)
- testutil.IsNil(t, err)
- testutil.IsNotNil(t, logger)
+ assert.Nil(t, err)
+ assert.NotNil(t, logger)
defer ctx.Logger().Close()
_, err = os.ReadDir(ctx.BuildDirectory())
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
})
t.Run("should correctly cleanup an app directory", func(t *testing.T) {
manager := sut()
ctx, err := manager.PrepareBuild(context.Background(), depl)
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
ctx.Logger().Close() // Do not defer or else the directory will be locked
err = manager.Cleanup(context.Background(), app.ID())
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
_, err = os.ReadDir(ctx.BuildDirectory())
- testutil.IsTrue(t, os.IsNotExist(err))
+ assert.True(t, os.IsNotExist(err))
})
}
diff --git a/internal/deployment/infra/artifact/logger.go b/internal/deployment/infra/artifact/logger.go
index 8ea5edf2..6e8cd972 100644
--- a/internal/deployment/infra/artifact/logger.go
+++ b/internal/deployment/infra/artifact/logger.go
@@ -41,5 +41,5 @@ func (l *stepLogger) Close() error {
}
func (l *stepLogger) print(prefix string, format string, args []any) {
- l.Write([]byte(prefix + " " + fmt.Sprintf(format, args...) + "\n"))
+ _, _ = l.Write([]byte(prefix + " " + fmt.Sprintf(format, args...) + "\n"))
}
diff --git a/internal/deployment/infra/memory/apps.go b/internal/deployment/infra/memory/apps.go
deleted file mode 100644
index 77606b18..00000000
--- a/internal/deployment/infra/memory/apps.go
+++ /dev/null
@@ -1,203 +0,0 @@
-package memory
-
-import (
- "context"
-
- "github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/apperr"
- "github.com/YuukanOO/seelf/pkg/event"
- "github.com/YuukanOO/seelf/pkg/monad"
-)
-
-type (
- AppsStore interface {
- domain.AppsReader
- domain.AppsWriter
- }
-
- appsStore struct {
- apps []*appData
- }
-
- appData struct {
- id domain.AppID
- name domain.AppName
- productionTarget domain.TargetID
- stagingTarget domain.TargetID
- value *domain.App
- }
-)
-
-func NewAppsStore(existingApps ...*domain.App) AppsStore {
- s := &appsStore{}
-
- s.Write(context.Background(), existingApps...)
-
- return s
-}
-
-func (s *appsStore) CheckAppNamingAvailability(
- ctx context.Context,
- name domain.AppName,
- production domain.EnvironmentConfig,
- staging domain.EnvironmentConfig,
-) (domain.EnvironmentConfigRequirement, domain.EnvironmentConfigRequirement, error) {
- var productionTaken, stagingTaken bool
-
- for _, app := range s.apps {
- if app.name != name {
- continue
- }
-
- if app.productionTarget == production.Target() {
- productionTaken = true
- }
-
- if app.stagingTarget == staging.Target() {
- stagingTaken = true
- }
- }
-
- return domain.NewEnvironmentConfigRequirement(production, true, !productionTaken),
- domain.NewEnvironmentConfigRequirement(staging, true, !stagingTaken),
- nil
-}
-
-func (s *appsStore) CheckAppNamingAvailabilityByID(
- ctx context.Context,
- id domain.AppID,
- production monad.Maybe[domain.EnvironmentConfig],
- staging monad.Maybe[domain.EnvironmentConfig],
-) (
- productionRequirement domain.EnvironmentConfigRequirement,
- stagingRequirement domain.EnvironmentConfigRequirement,
- err error,
-) {
- productionValue, hasProductionTarget := production.TryGet()
- stagingValue, hasStagingTarget := staging.TryGet()
-
- // No input, no check!
- if !hasProductionTarget && !hasStagingTarget {
- return productionRequirement, stagingRequirement, nil
- }
-
- // Retrieve app name by its ID
- var name domain.AppName
-
- for _, app := range s.apps {
- if app.id == id {
- name = app.name
- break
- }
- }
-
- if name == "" {
- return productionRequirement, stagingRequirement, apperr.ErrNotFound
- }
-
- var productionTaken, stagingTaken bool
-
- // And check if an app on the target and env already exists
- for _, app := range s.apps {
- if app.id == id || app.name != name {
- continue
- }
-
- if hasProductionTarget && app.productionTarget == productionValue.Target() {
- productionTaken = true
- }
-
- if hasStagingTarget && app.stagingTarget == stagingValue.Target() {
- stagingTaken = true
- }
- }
-
- if hasProductionTarget {
- productionRequirement = domain.NewEnvironmentConfigRequirement(productionValue, true, !productionTaken)
- }
-
- if hasStagingTarget {
- stagingRequirement = domain.NewEnvironmentConfigRequirement(stagingValue, true, !stagingTaken)
- }
-
- return productionRequirement, stagingRequirement, nil
-}
-
-func (s *appsStore) HasAppsOnTarget(ctx context.Context, target domain.TargetID) (domain.HasAppsOnTarget, error) {
- for _, app := range s.apps {
- if app.productionTarget == target || app.stagingTarget == target {
- return true, nil
- }
- }
-
- return false, nil
-}
-
-func (s *appsStore) GetByID(ctx context.Context, id domain.AppID) (domain.App, error) {
- for _, app := range s.apps {
- if app.id == id {
- return *app.value, nil
- }
- }
-
- return domain.App{}, apperr.ErrNotFound
-}
-
-func (s *appsStore) Write(ctx context.Context, apps ...*domain.App) error {
- for _, app := range apps {
- for _, e := range event.Unwrap(app) {
- switch evt := e.(type) {
- case domain.AppCreated:
- var exist bool
- for _, a := range s.apps {
- if a.id == evt.ID {
- exist = true
- break
- }
- }
-
- if exist {
- continue
- }
-
- s.apps = append(s.apps, &appData{
- id: evt.ID,
- name: evt.Name,
- productionTarget: evt.Production.Target(),
- stagingTarget: evt.Staging.Target(),
- value: app,
- })
- case domain.AppEnvChanged:
- for _, a := range s.apps {
- if a.id == app.ID() {
- switch evt.Environment {
- case domain.Production:
- a.productionTarget = evt.Config.Target()
- case domain.Staging:
- a.stagingTarget = evt.Config.Target()
- }
- *a.value = *app
- break
- }
- }
- case domain.AppDeleted:
- for i, a := range s.apps {
- if a.id == app.ID() {
- *a.value = *app
- s.apps = append(s.apps[:i], s.apps[i+1:]...)
- break
- }
- }
- default:
- for _, a := range s.apps {
- if a.id == app.ID() {
- *a.value = *app
- break
- }
- }
- }
- }
- }
-
- return nil
-}
diff --git a/internal/deployment/infra/memory/deployments.go b/internal/deployment/infra/memory/deployments.go
deleted file mode 100644
index 9e1c3ff6..00000000
--- a/internal/deployment/infra/memory/deployments.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package memory
-
-import (
- "context"
-
- "github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/apperr"
- shared "github.com/YuukanOO/seelf/pkg/domain"
- "github.com/YuukanOO/seelf/pkg/event"
-)
-
-type (
- DeploymentsStore interface {
- domain.DeploymentsReader
- domain.DeploymentsWriter
- }
-
- deploymentsStore struct {
- deployments []*deploymentData
- }
-
- deploymentData struct {
- id domain.DeploymentID
- value *domain.Deployment
- state domain.DeploymentState
- }
-)
-
-func NewDeploymentsStore(existingDeployments ...*domain.Deployment) DeploymentsStore {
- s := &deploymentsStore{}
-
- s.Write(context.Background(), existingDeployments...)
-
- return s
-}
-
-func (s *deploymentsStore) GetByID(ctx context.Context, id domain.DeploymentID) (domain.Deployment, error) {
- for _, depl := range s.deployments {
- if depl.id == id {
- return *depl.value, nil
- }
- }
-
- return domain.Deployment{}, apperr.ErrNotFound
-}
-
-func (s *deploymentsStore) GetLastDeployment(ctx context.Context, id domain.AppID, env domain.Environment) (domain.Deployment, error) {
- var last *deploymentData
-
- for _, depl := range s.deployments {
- if depl.id.AppID() == id && depl.value.Config().Environment() == env {
- if last == nil || last.id.DeploymentNumber() < depl.id.DeploymentNumber() {
- last = depl
- }
- }
- }
-
- if last == nil {
- return domain.Deployment{}, apperr.ErrNotFound
- }
-
- return *last.value, nil
-
-}
-
-func (s *deploymentsStore) GetNextDeploymentNumber(ctx context.Context, appid domain.AppID) (domain.DeploymentNumber, error) {
- count := 0
-
- for _, depl := range s.deployments {
- if depl.id.AppID() == appid {
- count += 1
- }
- }
-
- return domain.DeploymentNumber(count + 1), nil
-}
-
-func (s *deploymentsStore) HasRunningOrPendingDeploymentsOnTarget(ctx context.Context, target domain.TargetID) (domain.HasRunningOrPendingDeploymentsOnTarget, error) {
- for _, d := range s.deployments {
- if d.value.Config().Target() == target && (d.state.Status() == domain.DeploymentStatusRunning || d.state.Status() == domain.DeploymentStatusPending) {
- return true, nil
- }
- }
-
- return false, nil
-}
-
-func (s *deploymentsStore) HasDeploymentsOnAppTargetEnv(ctx context.Context, app domain.AppID, target domain.TargetID, env domain.Environment, ti shared.TimeInterval) (
- domain.HasRunningOrPendingDeploymentsOnAppTargetEnv,
- domain.HasSuccessfulDeploymentsOnAppTargetEnv,
- error,
-) {
- var (
- ongoing domain.HasRunningOrPendingDeploymentsOnAppTargetEnv
- successful domain.HasSuccessfulDeploymentsOnAppTargetEnv
- )
-
- for _, d := range s.deployments {
- if d.id.AppID() != app || d.value.Config().Target() != target || d.value.Config().Environment() != env {
- continue
- }
-
- switch d.state.Status() {
- case domain.DeploymentStatusSucceeded:
- if d.value.Requested().At().After(ti.From()) && d.value.Requested().At().Before(ti.To()) {
- successful = true
- }
- case domain.DeploymentStatusRunning, domain.DeploymentStatusPending:
- ongoing = true
- }
- }
-
- return ongoing, successful, nil
-}
-
-func (s *deploymentsStore) FailDeployments(ctx context.Context, reason error, criterias domain.FailCriterias) error {
- panic("not implemented")
-}
-
-func (s *deploymentsStore) Write(ctx context.Context, deployments ...*domain.Deployment) error {
- for _, depl := range deployments {
- for _, e := range event.Unwrap(depl) {
- switch evt := e.(type) {
- case domain.DeploymentCreated:
- var exist bool
- for _, a := range s.deployments {
- if a.id == evt.ID {
- exist = true
- break
- }
- }
-
- if exist {
- continue
- }
-
- s.deployments = append(s.deployments, &deploymentData{
- id: evt.ID,
- value: depl,
- state: evt.State,
- })
- case domain.DeploymentStateChanged:
- for _, d := range s.deployments {
- if d.id == depl.ID() {
- *d.value = *depl
- d.state = evt.State
- break
- }
- }
- default:
- for _, d := range s.deployments {
- if d.id == depl.ID() {
- *d.value = *depl
- break
- }
- }
- }
- }
- }
-
- return nil
-}
diff --git a/internal/deployment/infra/memory/registries.go b/internal/deployment/infra/memory/registries.go
deleted file mode 100644
index 6889751b..00000000
--- a/internal/deployment/infra/memory/registries.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package memory
-
-import (
- "context"
- "slices"
-
- "github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/apperr"
- "github.com/YuukanOO/seelf/pkg/event"
-)
-
-type (
- RegistriesStore interface {
- domain.RegistriesReader
- domain.RegistriesWriter
- }
-
- registriesStore struct {
- registries []*registryData
- }
-
- registryData struct {
- id domain.RegistryID
- value *domain.Registry
- }
-)
-
-func NewRegistriesStore(existingApps ...*domain.Registry) RegistriesStore {
- s := ®istriesStore{}
-
- s.Write(context.Background(), existingApps...)
-
- return s
-}
-
-func (s *registriesStore) CheckUrlAvailability(ctx context.Context, domainUrl domain.Url, excluded ...domain.RegistryID) (domain.RegistryUrlRequirement, error) {
- var registry *domain.Registry
-
- for _, t := range s.registries {
- if t.value.Url() == domainUrl {
- registry = t.value
- break
- }
- }
-
- return domain.NewRegistryUrlRequirement(domainUrl, registry == nil || slices.Contains(excluded, registry.ID())), nil
-}
-
-func (s *registriesStore) GetByID(ctx context.Context, id domain.RegistryID) (domain.Registry, error) {
- for _, r := range s.registries {
- if r.id == id {
- return *r.value, nil
- }
- }
-
- return domain.Registry{}, apperr.ErrNotFound
-}
-
-func (s *registriesStore) GetAll(ctx context.Context) ([]domain.Registry, error) {
- var registries []domain.Registry
-
- for _, r := range s.registries {
- registries = append(registries, *r.value)
- }
-
- return registries, nil
-}
-
-func (s *registriesStore) Write(ctx context.Context, registries ...*domain.Registry) error {
- for _, reg := range registries {
- for _, e := range event.Unwrap(reg) {
- switch evt := e.(type) {
- case domain.RegistryCreated:
- var exist bool
- for _, r := range s.registries {
- if r.id == evt.ID {
- exist = true
- break
- }
- }
-
- if exist {
- continue
- }
-
- s.registries = append(s.registries, ®istryData{
- id: evt.ID,
- value: reg,
- })
- case domain.RegistryDeleted:
- for i, r := range s.registries {
- if r.id == reg.ID() {
- *r.value = *reg
- s.registries = append(s.registries[:i], s.registries[i+1:]...)
- break
- }
- }
- default:
- for _, r := range s.registries {
- if r.id == reg.ID() {
- *r.value = *reg
- break
- }
- }
- }
- }
- }
-
- return nil
-}
diff --git a/internal/deployment/infra/memory/targets.go b/internal/deployment/infra/memory/targets.go
deleted file mode 100644
index 75fde1a7..00000000
--- a/internal/deployment/infra/memory/targets.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package memory
-
-import (
- "context"
- "slices"
-
- "github.com/YuukanOO/seelf/internal/deployment/domain"
- "github.com/YuukanOO/seelf/pkg/apperr"
- "github.com/YuukanOO/seelf/pkg/event"
-)
-
-type (
- TargetsStore interface {
- domain.TargetsReader
- domain.TargetsWriter
- }
-
- targetsStore struct {
- targets []*targetData
- }
-
- targetData struct {
- id domain.TargetID
- domain domain.Url
- value *domain.Target
- }
-)
-
-func NewTargetsStore(existingTargets ...*domain.Target) TargetsStore {
- s := &targetsStore{}
-
- s.Write(context.Background(), existingTargets...)
-
- return s
-}
-
-func (s *targetsStore) CheckUrlAvailability(ctx context.Context, domainUrl domain.Url, excluded ...domain.TargetID) (domain.TargetUrlRequirement, error) {
- var target *domain.Target
-
- for _, t := range s.targets {
- if t.domain.String() == domainUrl.String() {
- target = t.value
- break
- }
- }
-
- return domain.NewTargetUrlRequirement(domainUrl, target == nil || slices.Contains(excluded, target.ID())), nil
-}
-
-func (s *targetsStore) CheckConfigAvailability(ctx context.Context, config domain.ProviderConfig, excluded ...domain.TargetID) (domain.ProviderConfigRequirement, error) {
- var target *domain.Target
-
- for _, t := range s.targets {
- if t.value.Provider().Fingerprint() == config.Fingerprint() {
- target = t.value
- break
- }
- }
-
- return domain.NewProviderConfigRequirement(config, target == nil || slices.Contains(excluded, target.ID())), nil
-}
-
-func (s *targetsStore) GetLocalTarget(ctx context.Context) (domain.Target, error) {
- for _, t := range s.targets {
- if t.value.Provider().Fingerprint() == "" {
- return *t.value, nil
- }
- }
-
- return domain.Target{}, apperr.ErrNotFound
-}
-
-func (s *targetsStore) GetByID(ctx context.Context, id domain.TargetID) (domain.Target, error) {
- for _, t := range s.targets {
- if t.id == id {
- return *t.value, nil
- }
- }
-
- return domain.Target{}, apperr.ErrNotFound
-}
-
-func (s *targetsStore) Write(ctx context.Context, targets ...*domain.Target) error {
- for _, target := range targets {
- for _, e := range event.Unwrap(target) {
- switch evt := e.(type) {
- case domain.TargetCreated:
- var exist bool
- for _, a := range s.targets {
- if a.id == evt.ID {
- exist = true
- break
- }
- }
-
- if exist {
- continue
- }
-
- s.targets = append(s.targets, &targetData{
- id: evt.ID,
- domain: evt.Url,
- value: target,
- })
- case domain.TargetUrlChanged:
- for _, t := range s.targets {
- if t.id == evt.ID {
- t.domain = evt.Url
- *t.value = *target
- break
- }
- }
- case domain.TargetDeleted:
- for i, t := range s.targets {
- if t.id == target.ID() {
- *t.value = *target
- s.targets = append(s.targets[:i], s.targets[i+1:]...)
- break
- }
- }
- default:
- for _, t := range s.targets {
- if t.id == target.ID() {
- *t.value = *target
- break
- }
- }
- }
- }
- }
-
- return nil
-}
diff --git a/internal/deployment/infra/mod.go b/internal/deployment/infra/mod.go
index ee0db6f4..568ec385 100644
--- a/internal/deployment/infra/mod.go
+++ b/internal/deployment/infra/mod.go
@@ -124,7 +124,7 @@ func Setup(
}
// Fail running deployments in case of a hard reset.
- return deploymentsStore.FailDeployments(context.Background(), errors.New("server_reset"), domain.FailCriterias{
+ return deploymentsStore.FailDeployments(context.Background(), errors.New("server_reset"), domain.FailCriteria{
Status: monad.Value(domain.DeploymentStatusRunning),
})
}
diff --git a/internal/deployment/infra/provider/docker/client.go b/internal/deployment/infra/provider/docker/client.go
index 6951f8c6..3158fe18 100644
--- a/internal/deployment/infra/provider/docker/client.go
+++ b/internal/deployment/infra/provider/docker/client.go
@@ -12,10 +12,10 @@ import (
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
- "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
dclient "github.com/docker/docker/client"
)
@@ -142,7 +142,7 @@ func (c *client) RemoveResources(ctx context.Context, criteria filters.Args) err
}
// List and remove all networks
- networks, err := c.api.NetworkList(ctx, types.NetworkListOptions{
+ networks, err := c.api.NetworkList(ctx, network.ListOptions{
Filters: criteria,
})
diff --git a/internal/deployment/infra/provider/docker/data_test.go b/internal/deployment/infra/provider/docker/data_test.go
index 16a0e004..d7294f10 100644
--- a/internal/deployment/infra/provider/docker/data_test.go
+++ b/internal/deployment/infra/provider/docker/data_test.go
@@ -6,9 +6,9 @@ import (
"github.com/YuukanOO/seelf/internal/deployment/domain"
"github.com/YuukanOO/seelf/internal/deployment/infra/provider/docker"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/ssh"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Data(t *testing.T) {
@@ -56,7 +56,7 @@ func Test_Data(t *testing.T) {
t.Run(fmt.Sprintf("%v", test), func(t *testing.T) {
got := test.a.Equals(test.b)
- testutil.Equals(t, test.expected, got)
+ assert.Equal(t, test.expected, got)
})
}
})
diff --git a/internal/deployment/infra/provider/docker/deployment.go b/internal/deployment/infra/provider/docker/deployment.go
index 09f7c07e..a0e07d22 100644
--- a/internal/deployment/infra/provider/docker/deployment.go
+++ b/internal/deployment/infra/provider/docker/deployment.go
@@ -18,31 +18,41 @@ import (
"golang.org/x/exp/maps"
)
-type deploymentProjectBuilder struct {
- sourceDir string
- composePath string
- networkName string
- services domain.Services
- project *types.Project
- config domain.DeploymentConfig
- logger domain.DeploymentLogger
- labels types.Labels
- isDefaultSubdomainAvailable bool
- routersByPort map[string]domain.Router
-}
+type (
+ DeploymentProjectBuilder interface {
+ Build(context.Context) (*types.Project, domain.Services, error)
+ }
-func newDeploymentProjectBuilder(ctx domain.DeploymentContext, depl domain.Deployment) *deploymentProjectBuilder {
- config := depl.Config()
+ deploymentProjectBuilder struct {
+ exposedManually bool
+ sourceDir string
+ composePath string
+ networkName string
+ config domain.ConfigSnapshot
+ logger domain.DeploymentLogger
+ labels types.Labels
+ routersByPort map[string]domain.Router
+ services domain.Services
+ project *types.Project
+ }
+)
+
+func newDeploymentProjectBuilder(
+ ctx domain.DeploymentContext,
+ deployment domain.Deployment,
+ target domain.Target,
+) DeploymentProjectBuilder {
+ config := deployment.Config()
return &deploymentProjectBuilder{
- isDefaultSubdomainAvailable: true,
- sourceDir: ctx.BuildDirectory(),
- config: config,
- networkName: targetPublicNetworkName(config.Target()),
- logger: ctx.Logger(),
- routersByPort: make(map[string]domain.Router),
+ exposedManually: target.IsManual(),
+ sourceDir: ctx.BuildDirectory(),
+ config: config,
+ networkName: targetPublicNetworkName(config.Target()),
+ logger: ctx.Logger(),
+ routersByPort: make(map[string]domain.Router),
labels: types.Labels{
- AppLabel: string(depl.ID().AppID()),
+ AppLabel: string(deployment.ID().AppID()),
TargetLabel: string(config.Target()),
EnvironmentLabel: string(config.Environment()),
},
@@ -111,11 +121,14 @@ func (b *deploymentProjectBuilder) findComposeFile() error {
func (b *deploymentProjectBuilder) loadProject(ctx context.Context) error {
b.logger.Stepf("reading project from %s", b.composePath)
- opts, err := cli.NewProjectOptions([]string{b.composePath},
+ loaders := []cli.ProjectOptionsFn{
cli.WithName(b.config.ProjectName()),
cli.WithNormalization(true),
cli.WithProfiles([]string{string(b.config.Environment())}),
- cli.WithLoadOptions(func(o *loader.Options) {
+ }
+
+ if !b.exposedManually {
+ loaders = append(loaders, cli.WithLoadOptions(func(o *loader.Options) {
o.Interpolate = &interpolation.Options{
TypeCastMapping: map[tree.Path]interpolation.Cast{
"services.*.ports.[]": func(value string) (any, error) {
@@ -123,8 +136,10 @@ func (b *deploymentProjectBuilder) loadProject(ctx context.Context) error {
},
},
}
- }),
- )
+ }))
+ }
+
+ opts, err := cli.NewProjectOptions([]string{b.composePath}, loaders...)
if err != nil {
b.logger.Error(err)
@@ -154,11 +169,13 @@ func (b *deploymentProjectBuilder) transform() {
b.project.DisabledServices = nil // Reset the list of disabled services or orphans created for an old profile will not be deleted
}
+ builder := b.config.ServicesBuilder()
+
// Let's transform the project to expose needed services
- // Here ServiceNames sort the services by alphabetical order
+ // Here ServiceNames sort the services by alphabetical order so we don't have to
for _, name := range b.project.ServiceNames() {
serviceDefinition := b.project.Services[name]
- service := b.config.NewService(serviceDefinition.Name, serviceDefinition.Image)
+ service := builder.AddService(serviceDefinition.Name, serviceDefinition.Image)
serviceName := service.Name()
if serviceDefinition.Restart == "" {
@@ -195,10 +212,9 @@ func (b *deploymentProjectBuilder) transform() {
}
}
- // No ports mapped, nothing to do
- if len(serviceDefinition.Ports) == 0 {
+ // No ports mapped or manual target, nothing to do
+ if b.exposedManually || len(serviceDefinition.Ports) == 0 {
b.project.Services[serviceName] = serviceDefinition
- b.services = append(b.services, service)
continue
}
@@ -215,19 +231,18 @@ func (b *deploymentProjectBuilder) transform() {
continue
}
+ port := domain.Port(portConfig.Target)
+
switch router {
case domain.RouterHttp:
- entrypoint = service.AddHttpEntrypoint(b.config, domain.Port(portConfig.Target), domain.HttpEntrypointOptions{
- Managed: httpMainEntryPointAvailable,
- UseDefaultSubdomain: b.isDefaultSubdomainAvailable,
- })
+ entrypoint = service.AddHttpEntrypoint(port, !httpMainEntryPointAvailable)
httpMainEntryPointAvailable = false
- serviceDefinition.Labels[SubdomainLabel] = entrypoint.Subdomain().MustGet()
+ serviceDefinition.Labels[SubdomainLabel] = entrypoint.Subdomain().Get("")
case domain.RouterTcp:
- entrypoint = service.AddTCPEntrypoint(domain.Port(portConfig.Target))
+ entrypoint = service.AddTCPEntrypoint(port, true)
serviceDefinition.Labels["traefik.tcp.routers."+string(entrypoint.Name())+".rule"] = "HostSNI(`*`)"
case domain.RouterUdp:
- entrypoint = service.AddUDPEntrypoint(domain.Port(portConfig.Target))
+ entrypoint = service.AddUDPEntrypoint(port, true)
default:
b.logger.Warnf("unsupported router type for service %s, the service will not be exposed", serviceName)
continue
@@ -240,7 +255,6 @@ func (b *deploymentProjectBuilder) transform() {
if !entrypoint.IsCustom() {
serviceDefinition.Labels["traefik."+routerName+".routers."+entrypointName+".entrypoints"] = httpMainEntryPoint
- b.isDefaultSubdomainAvailable = false
} else {
serviceDefinition.Labels[CustomEntrypointsLabel] = "true"
serviceDefinition.Labels["traefik."+routerName+".routers."+entrypointName+".entrypoints"] = entrypointName
@@ -261,9 +275,10 @@ func (b *deploymentProjectBuilder) transform() {
// Update the project definition and state
b.project.Services[serviceName] = serviceDefinition
- b.services = append(b.services, service)
}
+ b.services = builder.Services()
+
// Add labels to network and volumes to make it easy to find them
for name, network := range b.project.Networks {
network.Labels = appendLabels(network.Labels, b.labels)
@@ -276,6 +291,9 @@ func (b *deploymentProjectBuilder) transform() {
}
// Append the public seelf network to the project
+ if b.exposedManually {
+ return
+ }
if b.project.Networks == nil {
b.project.Networks = types.Networks{}
diff --git a/internal/deployment/infra/provider/docker/provider.go b/internal/deployment/infra/provider/docker/provider.go
index 22378143..aeffbd82 100644
--- a/internal/deployment/infra/provider/docker/provider.go
+++ b/internal/deployment/infra/provider/docker/provider.go
@@ -78,13 +78,14 @@ func New(logger log.Logger, configuration ...DockerOptions) Docker {
}
// Use the given compose service and cli instead of creating new ones. Used for testing.
-func WithDockerAndCompose(cli command.Cli, composeService api.Service) DockerOptions {
+func WithTestConfig(cli command.Cli, composeService api.Service, sshConfigPath string) DockerOptions {
return func(d *docker) {
d.client = &client{
cli: cli,
api: cli.Client(),
compose: composeService,
}
+ d.sshConfig = ssh.NewFileConfigurator(sshConfigPath)
}
}
@@ -172,6 +173,14 @@ func (d *docker) Setup(ctx context.Context, target domain.Target) (domain.Target
defer client.Close()
+ if target.IsManual() {
+ return nil, client.compose.Down(ctx, targetProjectName(target.ID()), api.DownOptions{
+ RemoveOrphans: true,
+ Images: "all",
+ Volumes: true,
+ })
+ }
+
project, assigned, err := newProxyProjectBuilder(client, target).Build(ctx)
if err != nil {
@@ -224,7 +233,7 @@ func (d *docker) Expose(ctx context.Context, target domain.Target, container str
func (d *docker) Deploy(
ctx context.Context,
deploymentCtx domain.DeploymentContext,
- depl domain.Deployment,
+ deployment domain.Deployment,
target domain.Target,
registries []domain.Registry,
) (domain.Services, error) {
@@ -244,7 +253,7 @@ func (d *docker) Deploy(
logger.Infof("using custom registries: %s", strings.Join(client.registries, ", "))
}
- project, services, err := newDeploymentProjectBuilder(deploymentCtx, depl).Build(ctx)
+ project, services, err := newDeploymentProjectBuilder(deploymentCtx, deployment, target).Build(ctx)
if err != nil {
return nil, err
@@ -267,19 +276,21 @@ func (d *docker) Deploy(
return nil, ErrComposeFailed
}
- if target.Url().UseSSL() {
- logger.Infof("you may have to wait for certificates to be generated before your app is available")
- }
+ if url, isManagedBySeelf := target.Url().TryGet(); isManagedBySeelf {
+ if url.UseSSL() {
+ logger.Infof("you may have to wait for certificates to be generated before your app is available")
+ }
- if len(services.CustomEntrypoints()) > 0 {
- logger.Infof("this deployment uses custom entrypoints. If this is the first time, you may have to wait a few seconds for the target to find available ports and expose them appropriately")
+ if len(services.CustomEntrypoints()) > 0 {
+ logger.Infof("this deployment uses custom entrypoints. If this is the first time, you may have to wait a few seconds for the target to find available ports and expose them appropriately")
+ }
}
prunedCount, err := client.PruneImages(ctx, filters.NewArgs(
filters.Arg("dangling", "true"),
- filters.Arg("label", AppLabel+"="+string(depl.ID().AppID())),
+ filters.Arg("label", AppLabel+"="+string(deployment.ID().AppID())),
filters.Arg("label", TargetLabel+"="+string(target.ID())),
- filters.Arg("label", EnvironmentLabel+"="+string(depl.Config().Environment())),
+ filters.Arg("label", EnvironmentLabel+"="+string(deployment.Config().Environment())),
))
if err != nil {
diff --git a/internal/deployment/infra/provider/docker/provider_test.go b/internal/deployment/infra/provider/docker/provider_test.go
index 5febbdee..e632f88f 100644
--- a/internal/deployment/infra/provider/docker/provider_test.go
+++ b/internal/deployment/infra/provider/docker/provider_test.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
+ "path/filepath"
"slices"
"strconv"
"strings"
@@ -12,19 +13,21 @@ import (
"github.com/YuukanOO/seelf/cmd/config"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/internal/deployment/infra/artifact"
"github.com/YuukanOO/seelf/internal/deployment/infra/provider/docker"
"github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/log"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
"github.com/YuukanOO/seelf/pkg/ssh"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
+ "github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
)
@@ -36,14 +39,14 @@ type options interface {
func Test_Provider(t *testing.T) {
logger := must.Panic(log.NewLogger())
- sut := func(opts options) (docker.Docker, *dockerMockService) {
+ arrange := func(opts options) (docker.Docker, *dockerMockService) {
mock := newMockService()
t.Cleanup(func() {
os.RemoveAll(opts.DataDir())
})
- return docker.New(logger, docker.WithDockerAndCompose(mock, mock)), mock
+ return docker.New(logger, docker.WithTestConfig(mock, mock, filepath.Join(opts.DataDir(), "config"))), mock
}
t.Run("should be able to prepare a docker provider config from a raw payload", func(t *testing.T) {
@@ -200,310 +203,582 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID
},
}
- provider, _ := sut(config.Default(config.WithTestDefaults()))
+ provider, _ := arrange(config.Default(config.WithTestDefaults()))
for _, tt := range tests {
t.Run(fmt.Sprintf("%v", tt.payload), func(t *testing.T) {
data, err := provider.Prepare(context.Background(), tt.payload, tt.existing...)
- testutil.IsNil(t, err)
- testutil.IsTrue(t, data.Equals(tt.expected))
+ assert.Nil(t, err)
+ assert.True(t, data.Equals(tt.expected))
})
}
})
- t.Run("should setup a new non-ssl target without custom entrypoints", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
-
- provider, mock := sut(config.Default(config.WithTestDefaults()))
-
- assigned, err := provider.Setup(context.Background(), target)
-
- testutil.IsNil(t, err)
- testutil.DeepEquals(t, domain.TargetEntrypointsAssigned{}, assigned)
- testutil.HasLength(t, mock.ups, 1)
- testutil.DeepEquals(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- },
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- "--entrypoints.http.address=:80",
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
- },
- Ports: []types.ServicePortConfig{
- {Target: 80, Published: "80"},
- },
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
- },
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ t.Run("should correctly setup needed stuff on a target", func(t *testing.T) {
+ t.Run("with automatic proxy, no-ssl, no custom entrypoints", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.DeepEqual(t, domain.TargetEntrypointsAssigned{}, assigned)
+ assert.HasLength(t, 1, mock.ups)
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ "--entrypoints.http.address=:80",
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: []types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ },
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- }, mock.ups[0].project)
- })
-
- t.Run("should setup a new ssl target without custom entrypoints", func(t *testing.T) {
- target := createTarget("https://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
-
- provider, mock := sut(config.Default(config.WithTestDefaults()))
-
- assigned, err := provider.Setup(context.Background(), target)
+ }, mock.ups[0].project)
+ })
- testutil.IsNil(t, err)
- testutil.DeepEquals(t, domain.TargetEntrypointsAssigned{}, assigned)
- testutil.HasLength(t, mock.ups, 1)
- testutil.DeepEquals(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- },
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- fmt.Sprintf("--certificatesresolvers.%s.acme.storage=/letsencrypt/acme.json", "seelf-resolver-"+targetIdLower),
- fmt.Sprintf("--certificatesresolvers.%s.acme.tlschallenge=true", "seelf-resolver-"+targetIdLower),
- "--entrypoints.http.address=:443",
- "--entrypoints.http.http.tls.certresolver=seelf-resolver-" + targetIdLower,
- "--entrypoints.insecure.address=:80",
- "--entrypoints.insecure.http.redirections.entryPoint.scheme=https",
- "--entrypoints.insecure.http.redirections.entryPoint.to=http",
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
- },
- Ports: []types.ServicePortConfig{
- {Target: 80, Published: "80"},
- {Target: 443, Published: "443"},
- },
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
- {Type: types.VolumeTypeVolume, Source: "letsencrypt", Target: "/letsencrypt"},
- },
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ t.Run("with automatic proxy, ssl, no custom entrypoints", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("https://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.DeepEqual(t, domain.TargetEntrypointsAssigned{}, assigned)
+ assert.HasLength(t, 1, mock.ups)
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ fmt.Sprintf("--certificatesresolvers.%s.acme.storage=/letsencrypt/acme.json", "seelf-resolver-"+targetIdLower),
+ fmt.Sprintf("--certificatesresolvers.%s.acme.tlschallenge=true", "seelf-resolver-"+targetIdLower),
+ "--entrypoints.http.address=:443",
+ "--entrypoints.http.http.tls.certresolver=seelf-resolver-" + targetIdLower,
+ "--entrypoints.insecure.address=:80",
+ "--entrypoints.insecure.http.redirections.entryPoint.scheme=https",
+ "--entrypoints.insecure.http.redirections.entryPoint.to=http",
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: []types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ {Target: 443, Published: "443"},
+ },
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ {Type: types.VolumeTypeVolume, Source: "letsencrypt", Target: "/letsencrypt"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- Volumes: types.Volumes{
- "letsencrypt": types.VolumeConfig{
- Name: "seelf-internal-" + targetIdLower + "_letsencrypt",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Volumes: types.Volumes{
+ "letsencrypt": types.VolumeConfig{
+ Name: "seelf-internal-" + targetIdLower + "_letsencrypt",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- }, mock.ups[0].project)
- })
-
- t.Run("should setup a target with custom entrypoints by finding available ports", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
- depl := createDeployment(target.ID(), "")
-
- service := depl.Config().NewService("app", "")
- tcp := service.AddTCPEntrypoint(5432)
- udp := service.AddUDPEntrypoint(5433)
-
- target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service})
-
- provider, mock := sut(config.Default(config.WithTestDefaults()))
-
- assigned, err := provider.Setup(context.Background(), target)
-
- testutil.IsNil(t, err)
- testutil.HasLength(t, mock.ups, 2)
- testutil.HasLength(t, mock.downs, 1)
-
- tcpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][tcp.Name()]
- udpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][udp.Name()]
-
- testutil.NotEquals(t, 0, tcpPort)
- testutil.NotEquals(t, 0, udpPort)
+ }, mock.ups[0].project)
+ })
- testutil.DeepEquals(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ t.Run("with automatic proxy, custom entrypoints", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ app := fixture.App(fixture.WithAppName("my-app"), fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ builder := deployment.Config().ServicesBuilder()
+ service := builder.AddService("app", "")
+ tcp := service.AddTCPEntrypoint(5432, true)
+ udp := service.AddUDPEntrypoint(5433, true)
+ target.ExposeEntrypoints(deployment.Config().AppID(), deployment.Config().Environment(), builder.Services())
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 2, mock.ups, "should have launch two projects since it has to find available ports")
+ assert.HasLength(t, 1, mock.downs)
+ tcpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][tcp.Name()]
+ udpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][udp.Name()]
+
+ assert.NotZero(t, tcpPort)
+ assert.NotZero(t, udpPort)
+
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ "--entrypoints.http.address=:80",
+ fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", tcp.Name(), tcpPort),
+ fmt.Sprintf("--entrypoints.%s.address=:%d/udp", udp.Name(), udpPort),
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: sortedPorts([]types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
+ {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
+ }),
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- "--entrypoints.http.address=:80",
- fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", tcp.Name(), tcpPort),
- fmt.Sprintf("--entrypoints.%s.address=:%d/udp", udp.Name(), udpPort),
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
- Ports: sortedPorts([]types.ServicePortConfig{
- {Target: 80, Published: "80"},
- {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
- {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
- }),
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ }, mock.ups[1].project)
+ })
+
+ t.Run("with automatic proxy, custom entrypoints and already determined ports", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ app := fixture.App(fixture.WithAppName("my-app"), fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ builder := deployment.Config().ServicesBuilder()
+ service := builder.AddService("app", "")
+ tcp := service.AddTCPEntrypoint(5432, true)
+ udp := service.AddUDPEntrypoint(5433, true)
+ target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), builder.Services())
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.ID().AppID(): {
+ deployment.Config().Environment(): {
+ tcp.Name(): 5432,
+ udp.Name(): 5433,
},
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ },
+ }, nil)
+ newTcp := service.AddTCPEntrypoint(5434, true)
+ newUdp := service.AddUDPEntrypoint(5435, true)
+ target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), builder.Services())
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 2, mock.ups)
+ assert.HasLength(t, 1, mock.downs)
+ assert.Equal(t, 2, len(assigned[deployment.ID().AppID()][deployment.Config().Environment()]))
+
+ tcpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][newTcp.Name()]
+ udpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][newUdp.Name()]
+
+ assert.NotZero(t, tcpPort)
+ assert.NotZero(t, udpPort)
+
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ "--entrypoints.http.address=:80",
+ fmt.Sprintf("--entrypoints.%s.address=:5432/tcp", tcp.Name()),
+ fmt.Sprintf("--entrypoints.%s.address=:5433/udp", udp.Name()),
+ fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", newTcp.Name(), tcpPort),
+ fmt.Sprintf("--entrypoints.%s.address=:%d/udp", newUdp.Name(), udpPort),
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: sortedPorts([]types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ {Target: 5432, Published: "5432", Protocol: "tcp"},
+ {Target: 5433, Published: "5433", Protocol: "udp"},
+ {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
+ {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
+ }),
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- }, mock.ups[1].project)
- })
-
- t.Run("should setup a target with custom entrypoints by using provided ports if any", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
- depl := createDeployment(target.ID(), "")
-
- service := depl.Config().NewService("app", "")
- tcp := service.AddTCPEntrypoint(5432)
- udp := service.AddUDPEntrypoint(5433)
+ }, mock.ups[1].project)
+ })
- target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service})
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- depl.ID().AppID(): {
- depl.Config().Environment(): {
- tcp.Name(): 5432,
- udp.Name(): 5433,
- },
- },
- }, nil)
+ t.Run("with manual target, should not deploy the proxy and remove existing one", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
- newTcp := service.AddTCPEntrypoint(5434)
- newUdp := service.AddUDPEntrypoint(5435)
- target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service})
+ assigned, err := provider.Setup(context.Background(), target)
- provider, mock := sut(config.Default(config.WithTestDefaults()))
+ assert.Nil(t, err)
+ assert.True(t, assigned == nil)
+ assert.HasLength(t, 0, mock.ups)
+ assert.HasLength(t, 1, mock.downs)
+ assert.Equal(t, "seelf-internal-"+strings.ToLower(string(target.ID())), mock.downs[0].projectName)
+ })
+ })
- assigned, err := provider.Setup(context.Background(), target)
+ t.Run("should be able to process a deployment", func(t *testing.T) {
+ t.Run("should returns an error if no valid compose file was found", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ deployment := fixture.Deployment()
+ opts := config.Default(config.WithTestDefaults())
+ artifactManager := artifact.NewLocal(opts, logger)
+ ctx, err := artifactManager.PrepareBuild(context.Background(), deployment)
+ assert.Nil(t, err)
+ defer ctx.Logger().Close()
+ provider, _ := arrange(opts)
- testutil.IsNil(t, err)
- testutil.HasLength(t, mock.ups, 2)
- testutil.HasLength(t, mock.downs, 1)
- testutil.Equals(t, 2, len(assigned[depl.ID().AppID()][depl.Config().Environment()]))
+ _, err = provider.Deploy(context.Background(), ctx, deployment, target, nil)
- tcpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][newTcp.Name()]
- udpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][newUdp.Name()]
+ assert.ErrorIs(t, docker.ErrOpenComposeFileFailed, err)
+ })
- testutil.NotEquals(t, 0, tcpPort)
- testutil.NotEquals(t, 0, udpPort)
+ t.Run("should correctly transform the compose file if the target is configured as automatically exposing services", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ productionConfig := domain.NewEnvironmentConfig(target.ID())
+ productionConfig.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": domain.EnvVars{
+ "DSN": "postgres://prodapp:passprod@db/app?sslmode=disable",
+ },
+ "db": domain.EnvVars{
+ "POSTGRES_USER": "prodapp",
+ "POSTGRES_PASSWORD": "passprod",
+ },
+ })
+ app := fixture.App(
+ fixture.WithAppName("my-app"),
+ fixture.WithEnvironmentConfig(
+ productionConfig,
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithSourceData(raw.Data(`services:
+ sidecar:
+ image: traefik/whoami
+ profiles:
+ - production
+ app:
+ restart: unless-stopped
+ build: .
+ environment:
+ - DSN=postgres://app:apppa55word@db/app?sslmode=disable
+ depends_on:
+ - db
+ ports:
+ - "8080:8080"
+ - "8081:8081/udp"
+ - "8082:8082"
+ stagingonly:
+ image: traefik/whoami
+ ports:
+ - "8888:80"
+ profiles:
+ - staging
+ db:
+ restart: unless-stopped
+ image: postgres:14-alpine
+ volumes:
+ - dbdata:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_USER=app
+ - POSTGRES_PASSWORD=apppa55word
+ ports:
+ - "5432:5432/tcp"
+volumes:
+ dbdata:`)),
+ )
+ appIdLower := strings.ToLower(string(app.ID()))
+
+ // Prepare the build
+ opts := config.Default(config.WithTestDefaults())
+ artifactManager := artifact.NewLocal(opts, logger)
+ deploymentContext, err := artifactManager.PrepareBuild(context.Background(), deployment)
+ assert.Nil(t, err)
+ assert.Nil(t, raw.New().Fetch(context.Background(), deploymentContext, deployment))
+ defer deploymentContext.Logger().Close()
+ provider, mock := arrange(opts)
+
+ services, err := provider.Deploy(context.Background(), deploymentContext, deployment, target, nil)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 1, mock.ups)
+ assert.HasLength(t, 3, services)
+
+ assert.Equal(t, "app", services[0].Name())
+ assert.Equal(t, "db", services[1].Name())
+ assert.Equal(t, "sidecar", services[2].Name())
+
+ entrypoints := services.Entrypoints()
+ assert.HasLength(t, 4, entrypoints)
+ assert.Equal(t, 8080, entrypoints[0].Port())
+ assert.Equal(t, "http", entrypoints[0].Router())
+ assert.Equal(t, string(deployment.Config().AppName()), entrypoints[0].Subdomain().Get(""))
+ assert.Equal(t, 8081, entrypoints[1].Port())
+ assert.Equal(t, "udp", entrypoints[1].Router())
+ assert.Equal(t, 8082, entrypoints[2].Port())
+ assert.Equal(t, "http", entrypoints[2].Router())
+ assert.Equal(t, string(deployment.Config().AppName()), entrypoints[2].Subdomain().Get(""))
+ assert.Equal(t, 5432, entrypoints[3].Port())
+ assert.Equal(t, "tcp", entrypoints[3].Router())
+
+ project := mock.ups[0].project
+ expectedProjectName := fmt.Sprintf("%s-%s-%s", deployment.Config().AppName(), deployment.Config().Environment(), appIdLower)
+ expectedGatewayNetworkName := "seelf-gateway-" + strings.ToLower(string(target.ID()))
+ assert.Equal(t, expectedProjectName, project.Name)
+ assert.Equal(t, 3, len(project.Services))
+
+ for _, service := range project.Services {
+ switch service.Name {
+ case "sidecar":
+ assert.Equal(t, "traefik/whoami", service.Image)
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{}, service.Environment)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ case "app":
+ httpEntrypointName := string(entrypoints[0].Name())
+ udpEntrypointName := string(entrypoints[1].Name())
+ customHttpEntrypointName := string(entrypoints[2].Name())
+ dsn := deployment.Config().EnvironmentVariablesFor("app").MustGet()["DSN"]
+
+ assert.Equal(t, fmt.Sprintf("%s-%s/app:%s", deployment.Config().AppName(), appIdLower, deployment.Config().Environment()), service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ docker.SubdomainLabel: string(deployment.Config().AppName()),
+ fmt.Sprintf("traefik.http.routers.%s.entrypoints", httpEntrypointName): "http",
+ fmt.Sprintf("traefik.http.routers.%s.service", httpEntrypointName): httpEntrypointName,
+ fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", httpEntrypointName): "8080",
+ docker.CustomEntrypointsLabel: "true",
+ fmt.Sprintf("traefik.udp.routers.%s.entrypoints", udpEntrypointName): udpEntrypointName,
+ fmt.Sprintf("traefik.udp.routers.%s.service", udpEntrypointName): udpEntrypointName,
+ fmt.Sprintf("traefik.udp.services.%s.loadbalancer.server.port", udpEntrypointName): "8081",
+ fmt.Sprintf("traefik.http.routers.%s.entrypoints", customHttpEntrypointName): customHttpEntrypointName,
+ fmt.Sprintf("traefik.http.routers.%s.service", customHttpEntrypointName): customHttpEntrypointName,
+ fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", customHttpEntrypointName): "8082",
+ }, service.Labels)
+
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "DSN": &dsn,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ expectedGatewayNetworkName: nil,
+ }, service.Networks)
+ case "db":
+ entrypointName := string(entrypoints[3].Name())
+ postgresUser := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_USER"]
+ postgresPassword := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_PASSWORD"]
+
+ assert.Equal(t, "postgres:14-alpine", service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ fmt.Sprintf("traefik.tcp.routers.%s.rule", entrypointName): "HostSNI(`*`)",
+ docker.CustomEntrypointsLabel: "true",
+ fmt.Sprintf("traefik.tcp.routers.%s.entrypoints", entrypointName): entrypointName,
+ fmt.Sprintf("traefik.tcp.routers.%s.service", entrypointName): entrypointName,
+ fmt.Sprintf("traefik.tcp.services.%s.loadbalancer.server.port", entrypointName): "5432",
+ }, service.Labels)
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "POSTGRES_USER": &postgresUser,
+ "POSTGRES_PASSWORD": &postgresPassword,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ expectedGatewayNetworkName: nil,
+ }, service.Networks)
+ assert.DeepEqual(t, []types.ServiceVolumeConfig{
+ {
+ Type: types.VolumeTypeVolume,
+ Source: "dbdata",
+ Target: "/var/lib/postgresql/data",
+ Volume: &types.ServiceVolumeVolume{},
+ },
+ }, service.Volumes)
+ default:
+ t.Fatalf("unexpected service %s", service.Name)
+ }
+ }
- testutil.DeepEquals(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
+ assert.DeepEqual(t, types.Networks{
+ "default": {
+ Name: expectedProjectName + "_default",
Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- },
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- "--entrypoints.http.address=:80",
- fmt.Sprintf("--entrypoints.%s.address=:5432/tcp", tcp.Name()),
- fmt.Sprintf("--entrypoints.%s.address=:5433/udp", udp.Name()),
- fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", newTcp.Name(), tcpPort),
- fmt.Sprintf("--entrypoints.%s.address=:%d/udp", newUdp.Name(), udpPort),
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
- },
- Ports: sortedPorts([]types.ServicePortConfig{
- {Target: 80, Published: "80"},
- {Target: 5432, Published: "5432", Protocol: "tcp"},
- {Target: 5433, Published: "5433", Protocol: "udp"},
- {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
- {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
- }),
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
- },
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
+ expectedGatewayNetworkName: {
+ Name: expectedGatewayNetworkName,
+ External: true,
+ },
+ }, project.Networks)
+ assert.DeepEqual(t, types.Volumes{
+ "dbdata": {
+ Name: expectedProjectName + "_dbdata",
Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
},
},
- },
- }, mock.ups[1].project)
- })
+ }, project.Volumes)
+
+ assert.DeepEqual(t, filters.NewArgs(
+ filters.Arg("dangling", "true"),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.AppLabel, deployment.ID().AppID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.TargetLabel, target.ID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.EnvironmentLabel, deployment.Config().Environment())),
+ ), mock.pruneFilters)
+ })
- t.Run("should expose services from a compose file", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- depl := createDeployment(target.ID(), `services:
+ t.Run("should correctly transform the compose file if the target is configured with a manual proxy", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ productionConfig := domain.NewEnvironmentConfig(target.ID())
+ productionConfig.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": domain.EnvVars{
+ "DSN": "postgres://prodapp:passprod@db/app?sslmode=disable",
+ },
+ "db": domain.EnvVars{
+ "POSTGRES_USER": "prodapp",
+ "POSTGRES_PASSWORD": "passprod",
+ },
+ })
+ app := fixture.App(
+ fixture.WithAppName("my-app"),
+ fixture.WithEnvironmentConfig(
+ productionConfig,
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithSourceData(raw.Data(`services:
sidecar:
image: traefik/whoami
profiles:
@@ -536,196 +811,156 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID
ports:
- "5432:5432/tcp"
volumes:
- dbdata:`)
- appIdLower := strings.ToLower(string(depl.ID().AppID()))
-
- // Prepare the build
- opts := config.Default(config.WithTestDefaults())
- artifactManager := artifact.NewLocal(opts, logger)
- ctx, err := artifactManager.PrepareBuild(context.Background(), depl)
- testutil.IsNil(t, err)
- testutil.IsNil(t, raw.New().Fetch(context.Background(), ctx, depl))
-
- provider, mock := sut(opts)
-
- services, err := provider.Deploy(context.Background(), ctx, depl, target, nil)
-
- testutil.IsNil(t, err)
- testutil.HasLength(t, mock.ups, 1)
- testutil.HasLength(t, services, 3)
-
- testutil.Equals(t, "app", services[0].Name())
- testutil.Equals(t, "db", services[1].Name())
- testutil.Equals(t, "sidecar", services[2].Name())
-
- entrypoints := services.Entrypoints()
- testutil.HasLength(t, entrypoints, 4)
- testutil.Equals(t, 8080, entrypoints[0].Port())
- testutil.Equals(t, "http", entrypoints[0].Router())
- testutil.Equals(t, string(depl.Config().AppName()), entrypoints[0].Subdomain().Get(""))
- testutil.Equals(t, 8081, entrypoints[1].Port())
- testutil.Equals(t, "udp", entrypoints[1].Router())
- testutil.Equals(t, 8082, entrypoints[2].Port())
- testutil.Equals(t, "http", entrypoints[2].Router())
- testutil.Equals(t, string(depl.Config().AppName()), entrypoints[2].Subdomain().Get(""))
- testutil.Equals(t, 5432, entrypoints[3].Port())
- testutil.Equals(t, "tcp", entrypoints[3].Router())
-
- project := mock.ups[0].project
- expectedProjectName := fmt.Sprintf("%s-%s-%s", depl.Config().AppName(), depl.Config().Environment(), appIdLower)
- expectedGatewayNetworkName := "seelf-gateway-" + strings.ToLower(string(target.ID()))
- testutil.Equals(t, expectedProjectName, project.Name)
- testutil.Equals(t, 3, len(project.Services))
-
- for _, service := range project.Services {
- switch service.Name {
- case "sidecar":
- testutil.Equals(t, "traefik/whoami", service.Image)
- testutil.HasLength(t, service.Ports, 0)
- testutil.DeepEquals(t, types.MappingWithEquals{}, service.Environment)
- testutil.DeepEquals(t, types.Labels{
- docker.AppLabel: string(depl.ID().AppID()),
- docker.TargetLabel: string(target.ID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
- }, service.Labels)
- testutil.DeepEquals(t, map[string]*types.ServiceNetworkConfig{
- "default": nil,
- }, service.Networks)
- case "app":
- httpEntrypointName := string(entrypoints[0].Name())
- udpEntrypointName := string(entrypoints[1].Name())
- customHttpEntrypointName := string(entrypoints[2].Name())
- dsn := depl.Config().EnvironmentVariablesFor("app").MustGet()["DSN"]
-
- testutil.Equals(t, fmt.Sprintf("%s-%s/app:%s", depl.Config().AppName(), appIdLower, depl.Config().Environment()), service.Image)
- testutil.Equals(t, types.RestartPolicyUnlessStopped, service.Restart)
- testutil.DeepEquals(t, types.Labels{
- docker.AppLabel: string(depl.ID().AppID()),
- docker.TargetLabel: string(target.ID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
- docker.SubdomainLabel: string(depl.Config().AppName()),
- fmt.Sprintf("traefik.http.routers.%s.entrypoints", httpEntrypointName): "http",
- fmt.Sprintf("traefik.http.routers.%s.service", httpEntrypointName): httpEntrypointName,
- fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", httpEntrypointName): "8080",
- docker.CustomEntrypointsLabel: "true",
- fmt.Sprintf("traefik.udp.routers.%s.entrypoints", udpEntrypointName): udpEntrypointName,
- fmt.Sprintf("traefik.udp.routers.%s.service", udpEntrypointName): udpEntrypointName,
- fmt.Sprintf("traefik.udp.services.%s.loadbalancer.server.port", udpEntrypointName): "8081",
- fmt.Sprintf("traefik.http.routers.%s.entrypoints", customHttpEntrypointName): customHttpEntrypointName,
- fmt.Sprintf("traefik.http.routers.%s.service", customHttpEntrypointName): customHttpEntrypointName,
- fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", customHttpEntrypointName): "8082",
- }, service.Labels)
-
- testutil.HasLength(t, service.Ports, 0)
- testutil.DeepEquals(t, types.MappingWithEquals{
- "DSN": &dsn,
- }, service.Environment)
- testutil.DeepEquals(t, map[string]*types.ServiceNetworkConfig{
- "default": nil,
- expectedGatewayNetworkName: nil,
- }, service.Networks)
- case "db":
- entrypointName := string(entrypoints[3].Name())
- postgresUser := depl.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_USER"]
- postgresPassword := depl.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_PASSWORD"]
-
- testutil.Equals(t, "postgres:14-alpine", service.Image)
- testutil.Equals(t, types.RestartPolicyUnlessStopped, service.Restart)
- testutil.DeepEquals(t, types.Labels{
- docker.AppLabel: string(depl.ID().AppID()),
- docker.TargetLabel: string(target.ID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
- fmt.Sprintf("traefik.tcp.routers.%s.rule", entrypointName): "HostSNI(`*`)",
- docker.CustomEntrypointsLabel: "true",
- fmt.Sprintf("traefik.tcp.routers.%s.entrypoints", entrypointName): entrypointName,
- fmt.Sprintf("traefik.tcp.routers.%s.service", entrypointName): entrypointName,
- fmt.Sprintf("traefik.tcp.services.%s.loadbalancer.server.port", entrypointName): "5432",
- }, service.Labels)
- testutil.HasLength(t, service.Ports, 0)
- testutil.DeepEquals(t, types.MappingWithEquals{
- "POSTGRES_USER": &postgresUser,
- "POSTGRES_PASSWORD": &postgresPassword,
- }, service.Environment)
- testutil.DeepEquals(t, map[string]*types.ServiceNetworkConfig{
- "default": nil,
- expectedGatewayNetworkName: nil,
- }, service.Networks)
- testutil.DeepEquals(t, []types.ServiceVolumeConfig{
- {
- Type: types.VolumeTypeVolume,
- Source: "dbdata",
- Target: "/var/lib/postgresql/data",
- Volume: &types.ServiceVolumeVolume{},
- },
- }, service.Volumes)
- default:
- t.Fatalf("unexpected service %s", service.Name)
+ dbdata:`)),
+ )
+ appIdLower := strings.ToLower(string(app.ID()))
+
+ // Prepare the build
+ opts := config.Default(config.WithTestDefaults())
+ artifactManager := artifact.NewLocal(opts, logger)
+ deploymentContext, err := artifactManager.PrepareBuild(context.Background(), deployment)
+ assert.Nil(t, err)
+ assert.Nil(t, raw.New().Fetch(context.Background(), deploymentContext, deployment))
+ defer deploymentContext.Logger().Close()
+ provider, mock := arrange(opts)
+
+ services, err := provider.Deploy(context.Background(), deploymentContext, deployment, target, nil)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 1, mock.ups)
+ assert.HasLength(t, 3, services)
+
+ assert.Equal(t, "app", services[0].Name())
+ assert.Equal(t, "db", services[1].Name())
+ assert.Equal(t, "sidecar", services[2].Name())
+
+ assert.HasLength(t, 0, services.Entrypoints())
+
+ project := mock.ups[0].project
+ expectedProjectName := fmt.Sprintf("%s-%s-%s", deployment.Config().AppName(), deployment.Config().Environment(), appIdLower)
+ assert.Equal(t, expectedProjectName, project.Name)
+ assert.Equal(t, 3, len(project.Services))
+
+ for _, service := range project.Services {
+ switch service.Name {
+ case "sidecar":
+ assert.Equal(t, "traefik/whoami", service.Image)
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{}, service.Environment)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ case "app":
+ dsn := deployment.Config().EnvironmentVariablesFor("app").MustGet()["DSN"]
+
+ assert.Equal(t, fmt.Sprintf("%s-%s/app:%s", deployment.Config().AppName(), appIdLower, deployment.Config().Environment()), service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+
+ assert.DeepEqual(t, []types.ServicePortConfig{
+ {
+ Protocol: "tcp",
+ Mode: "ingress",
+ Target: 8080,
+ Published: "8080",
+ },
+ {
+ Protocol: "udp",
+ Mode: "ingress",
+ Target: 8081,
+ Published: "8081",
+ },
+ {
+ Protocol: "tcp",
+ Mode: "ingress",
+ Target: 8082,
+ Published: "8082",
+ },
+ }, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "DSN": &dsn,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ case "db":
+ postgresUser := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_USER"]
+ postgresPassword := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_PASSWORD"]
+
+ assert.Equal(t, "postgres:14-alpine", service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+ assert.DeepEqual(t, []types.ServicePortConfig{
+ {
+ Protocol: "tcp",
+ Mode: "ingress",
+ Target: 5432,
+ Published: "5432",
+ },
+ }, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "POSTGRES_USER": &postgresUser,
+ "POSTGRES_PASSWORD": &postgresPassword,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ assert.DeepEqual(t, []types.ServiceVolumeConfig{
+ {
+ Type: types.VolumeTypeVolume,
+ Source: "dbdata",
+ Target: "/var/lib/postgresql/data",
+ Volume: &types.ServiceVolumeVolume{},
+ },
+ }, service.Volumes)
+ default:
+ t.Fatalf("unexpected service %s", service.Name)
+ }
}
- }
- testutil.DeepEquals(t, types.Networks{
- "default": {
- Name: expectedProjectName + "_default",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- docker.AppLabel: string(depl.Config().AppID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
+ assert.DeepEqual(t, types.Networks{
+ "default": {
+ Name: expectedProjectName + "_default",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ },
},
- },
- expectedGatewayNetworkName: {
- Name: expectedGatewayNetworkName,
- External: true,
- },
- }, project.Networks)
- testutil.DeepEquals(t, types.Volumes{
- "dbdata": {
- Name: expectedProjectName + "_dbdata",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- docker.AppLabel: string(depl.Config().AppID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
+ }, project.Networks)
+ assert.DeepEqual(t, types.Volumes{
+ "dbdata": {
+ Name: expectedProjectName + "_dbdata",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ },
},
- },
- }, project.Volumes)
-
- testutil.DeepEquals(t, filters.NewArgs(
- filters.Arg("dangling", "true"),
- filters.Arg("label", fmt.Sprintf("%s=%s", docker.AppLabel, depl.ID().AppID())),
- filters.Arg("label", fmt.Sprintf("%s=%s", docker.TargetLabel, target.ID())),
- filters.Arg("label", fmt.Sprintf("%s=%s", docker.EnvironmentLabel, depl.Config().Environment())),
- ), mock.pruneFilters)
- })
-}
-
-func createTarget(url string) domain.Target {
- return must.Panic(domain.NewTarget(
- "a target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom(url)), true),
- domain.NewProviderConfigRequirement(docker.Data{}, true),
- "uid",
- ))
-}
-
-func createDeployment(target domain.TargetID, data string) domain.Deployment {
- productionConfig := domain.NewEnvironmentConfig(target)
- productionConfig.HasEnvironmentVariables(domain.ServicesEnv{
- "app": domain.EnvVars{
- "DSN": "postgres://prodapp:passprod@db/app?sslmode=disable",
- },
- "db": domain.EnvVars{
- "POSTGRES_USER": "prodapp",
- "POSTGRES_PASSWORD": "passprod",
- },
+ }, project.Volumes)
+
+ assert.DeepEqual(t, filters.NewArgs(
+ filters.Arg("dangling", "true"),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.AppLabel, deployment.ID().AppID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.TargetLabel, target.ID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.EnvironmentLabel, deployment.Config().Environment())),
+ ), mock.pruneFilters)
+ })
})
- app := must.Panic(domain.NewApp(
- "my-app",
- domain.NewEnvironmentConfigRequirement(productionConfig, true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target), true, true),
- "uid",
- ))
-
- return must.Panic(app.NewDeployment(1, raw.Data(data), domain.Production, "uid"))
}
func sortedPorts(ports []types.ServicePortConfig) []types.ServicePortConfig {
@@ -822,23 +1057,7 @@ func (d *dockerMockCli) ContainerInspect(_ context.Context, containerName string
return result, nil
}
-func (d *dockerMockCli) ImagesPrune(_ context.Context, criteria filters.Args) (dockertypes.ImagesPruneReport, error) {
+func (d *dockerMockCli) ImagesPrune(_ context.Context, criteria filters.Args) (image.PruneReport, error) {
d.parent.pruneFilters = criteria
- return dockertypes.ImagesPruneReport{}, nil
+ return image.PruneReport{}, nil
}
-
-// func (d *dockerMockService) ContainerList(context.Context, container.ListOptions) ([]dockertypes.Container, error) {
-// return nil, nil
-// }
-
-// func (d *dockerMockService) VolumeList(context.Context, volume.ListOptions) (volume.ListResponse, error) {
-// return volume.ListResponse{}, nil
-// }
-
-// func (d *dockerMockService) NetworkList(context.Context, dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) {
-// return nil, nil
-// }
-
-// func (d *dockerMockService) ImageList(context.Context, image.ListOptions) ([]image.Summary, error) {
-// return nil, nil
-// }
diff --git a/internal/deployment/infra/provider/docker/proxy.go b/internal/deployment/infra/provider/docker/proxy.go
index 32e8364f..2a1321ed 100644
--- a/internal/deployment/infra/provider/docker/proxy.go
+++ b/internal/deployment/infra/provider/docker/proxy.go
@@ -17,6 +17,10 @@ const (
)
type (
+ ProxyProjectBuilder interface {
+ Build(context.Context) (*types.Project, domain.TargetEntrypointsAssigned, error)
+ }
+
// Builder used to create a compose project with everything needed to deploy
// the proxy used to expose application entrypoints.
// It will handle the assignment of new entrypoints ports if needed.
@@ -44,23 +48,24 @@ type (
}
)
-func newProxyProjectBuilder(client *client, target domain.Target) *proxyProjectBuilder {
+func newProxyProjectBuilder(client *client, target domain.Target) ProxyProjectBuilder {
id := target.ID()
- idLower := strings.ToLower(string(id))
+ idLower := domain.TargetID(strings.ToLower(string(id)))
+ url := target.Url().MustGet()
b := &proxyProjectBuilder{
client: client,
target: string(id),
- host: target.Url().Host(),
+ host: url.Host(),
entrypoints: target.CustomEntrypoints(),
assigned: make(domain.TargetEntrypointsAssigned),
- networkName: targetPublicNetworkName(target.ID()),
- projectName: "seelf-internal-" + idLower,
+ networkName: targetPublicNetworkName(idLower),
+ projectName: targetProjectName(idLower),
labels: types.Labels{TargetLabel: string(id)},
}
- if target.Url().UseSSL() {
- b.certResolverName = "seelf-resolver-" + idLower
+ if url.UseSSL() {
+ b.certResolverName = "seelf-resolver-" + string(idLower)
}
return b
@@ -291,3 +296,8 @@ func ServicePortSortFunc(a, b types.ServicePortConfig) int {
func targetPublicNetworkName(id domain.TargetID) string {
return "seelf-gateway-" + strings.ToLower(string(id))
}
+
+// Retrieve the project name of a specific target
+func targetProjectName(id domain.TargetID) string {
+ return "seelf-internal-" + strings.ToLower(string(id))
+}
diff --git a/internal/deployment/infra/provider/facade.go b/internal/deployment/infra/provider/facade.go
index d9b91d8b..8df1501b 100644
--- a/internal/deployment/infra/provider/facade.go
+++ b/internal/deployment/infra/provider/facade.go
@@ -33,14 +33,14 @@ func (f *facade) Prepare(ctx context.Context, payload any, existing ...domain.Pr
return nil, domain.ErrNoValidProviderFound
}
-func (f *facade) Deploy(ctx context.Context, info domain.DeploymentContext, depl domain.Deployment, target domain.Target, registries []domain.Registry) (domain.Services, error) {
+func (f *facade) Deploy(ctx context.Context, info domain.DeploymentContext, deployment domain.Deployment, target domain.Target, registries []domain.Registry) (domain.Services, error) {
provider, err := f.providerForTarget(target)
if err != nil {
return nil, err
}
- return provider.Deploy(ctx, info, depl, target, registries)
+ return provider.Deploy(ctx, info, deployment, target, registries)
}
func (f *facade) Setup(ctx context.Context, target domain.Target) (domain.TargetEntrypointsAssigned, error) {
diff --git a/internal/deployment/infra/provider/facade_test.go b/internal/deployment/infra/provider/facade_test.go
index 83eb670e..014bc92e 100644
--- a/internal/deployment/infra/provider/facade_test.go
+++ b/internal/deployment/infra/provider/facade_test.go
@@ -5,78 +5,64 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/internal/deployment/infra/provider"
- "github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_Facade(t *testing.T) {
- env := domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true)
- app := must.Panic(domain.NewApp("app", env, env, "uid"))
- depl := must.Panic(app.NewDeployment(1, dummySourceData{}, domain.Production, "uid"))
- url := domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)
- providerConfig := domain.NewProviderConfigRequirement(dummyProviderConfig{}, true)
- target := must.Panic(domain.NewTarget("target", url, providerConfig, "uid"))
-
t.Run("should return an error if no provider can handle the payload", func(t *testing.T) {
sut := provider.NewFacade()
_, err := sut.Prepare(context.Background(), "payload")
- testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err)
+ assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
t.Run("should return an error if no provider can handle the deployment", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
+ depl := fixture.Deployment()
_, err := sut.Deploy(context.Background(), domain.DeploymentContext{}, depl, target, nil)
- testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err)
+ assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
t.Run("should return an error if no provider can configure the target", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
_, err := sut.Setup(context.Background(), target)
- testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err)
+ assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
t.Run("should return an error if no provider can unconfigure the target", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
err := sut.RemoveConfiguration(context.Background(), target)
- testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err)
+ assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
t.Run("should return an error if no provider can cleanup the target", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
err := sut.CleanupTarget(context.Background(), target, domain.CleanupStrategyDefault)
- testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err)
+ assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
t.Run("should return an error if no provider can cleanup the app", func(t *testing.T) {
sut := provider.NewFacade()
+ app := fixture.App()
+ target := fixture.Target()
err := sut.Cleanup(context.Background(), app.ID(), target, domain.Production, domain.CleanupStrategyDefault)
- testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err)
+ assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
}
-
-type (
- dummyProviderConfig struct {
- domain.ProviderConfig
- }
-
- dummySourceData struct {
- domain.SourceData
- }
-)
-
-func (d dummyProviderConfig) Kind() string { return "dummy" }
-func (d dummySourceData) Kind() string { return "dummy" }
-func (d dummySourceData) NeedVersionControl() bool { return false }
diff --git a/internal/deployment/infra/source/git/data.go b/internal/deployment/infra/source/git/data.go
index 6f4a1e77..97aa7f00 100644
--- a/internal/deployment/infra/source/git/data.go
+++ b/internal/deployment/infra/source/git/data.go
@@ -25,7 +25,7 @@ func init() {
})
// Here the registered discriminated type is the same since there are no unexposed fields and
- // it also handle the retrocompatibility with the old payload format.
+ // it also handle the retro-compatibility with the old payload format.
get_deployment.SourceDataTypes.Register(Data{}, func(s string) (get_deployment.SourceData, error) {
return tryParseGitData(s)
})
diff --git a/internal/deployment/infra/sqlite/deployments.go b/internal/deployment/infra/sqlite/deployments.go
index 67a098b0..db61b94d 100644
--- a/internal/deployment/infra/sqlite/deployments.go
+++ b/internal/deployment/infra/sqlite/deployments.go
@@ -131,7 +131,7 @@ func (s *deploymentsStore) HasDeploymentsOnAppTargetEnv(ctx context.Context, app
domain.HasSuccessfulDeploymentsOnAppTargetEnv(c.successful), err
}
-func (s *deploymentsStore) FailDeployments(ctx context.Context, reason error, criterias domain.FailCriterias) error {
+func (s *deploymentsStore) FailDeployments(ctx context.Context, reason error, criterias domain.FailCriteria) error {
now := time.Now().UTC()
return builder.Update("deployments", builder.Values{
diff --git a/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql b/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql
index 924c88e6..2f588c5b 100644
--- a/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql
+++ b/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql
@@ -73,6 +73,7 @@ SELECT
FROM targets;
-- Rename the old apps table since it will be recreated with proper NOT NULL columns
+-- I should have used a temporary table
ALTER TABLE apps RENAME TO tmp_apps;
-- Create the new apps table with proper columns
@@ -146,6 +147,7 @@ SELECT
FROM tmp_apps;
-- Do the same for deployments
+-- I should have used a temporary table
ALTER TABLE deployments RENAME TO tmp_deployments;
CREATE TABLE deployments (
diff --git a/internal/deployment/infra/sqlite/migrations/1726473707_target_url_optional.up.sql b/internal/deployment/infra/sqlite/migrations/1726473707_target_url_optional.up.sql
new file mode 100644
index 00000000..629a32f9
--- /dev/null
+++ b/internal/deployment/infra/sqlite/migrations/1726473707_target_url_optional.up.sql
@@ -0,0 +1,55 @@
+-- since we cannot change the url nullable property easily, we have to do this steps
+CREATE TEMPORARY TABLE tmp_targets AS
+SELECT *
+FROM targets;
+
+CREATE TEMPORARY TABLE tmp_apps AS
+SELECT *
+FROM apps;
+
+CREATE TEMPORARY TABLE tmp_deployments AS
+SELECT *
+FROM deployments;
+
+DELETE FROM deployments;
+DELETE FROM apps;
+DROP TABLE targets;
+
+CREATE TABLE targets (
+ id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ url TEXT NULL,
+ provider_kind TEXT NOT NULL,
+ provider_fingerprint TEXT NOT NULL,
+ provider TEXT NOT NULL,
+ state_status INTEGER NOT NULL,
+ state_version DATETIME NOT NULL,
+ state_errcode TEXT NULL,
+ state_last_ready_version DATETIME NULL,
+ cleanup_requested_at DATETIME NULL,
+ cleanup_requested_by TEXT NULL,
+ created_at DATETIME NOT NULL,
+ created_by TEXT NOT NULL,
+ entrypoints TEXT NOT NULL DEFAULT '{}',
+
+ CONSTRAINT pk_targets PRIMARY KEY(id),
+ CONSTRAINT fk_targets_created_by FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE CASCADE,
+ CONSTRAINT unique_targets_url UNIQUE(url), -- unique url among all targets
+ CONSTRAINT unique_targets_provider_fingerprint UNIQUE(provider_fingerprint) -- unique provider fingerprint
+);
+
+INSERT INTO targets
+SELECT *
+FROM tmp_targets;
+
+INSERT INTO apps
+SELECT *
+FROM tmp_apps;
+
+INSERT INTO deployments
+SELECT *
+FROM tmp_deployments;
+
+DROP TABLE tmp_targets;
+DROP TABLE tmp_apps;
+DROP TABLE tmp_deployments;
\ No newline at end of file
diff --git a/internal/deployment/infra/sqlite/targets.go b/internal/deployment/infra/sqlite/targets.go
index b3b01afc..5eee1ae2 100644
--- a/internal/deployment/infra/sqlite/targets.go
+++ b/internal/deployment/infra/sqlite/targets.go
@@ -99,7 +99,6 @@ func (s *targetsStore) Write(c context.Context, targets ...*domain.Target) error
Insert("targets", builder.Values{
"id": evt.ID,
"name": evt.Name,
- "url": evt.Url,
"provider_kind": evt.Provider.Kind(),
"provider_fingerprint": evt.Provider.Fingerprint(),
"provider": evt.Provider,
@@ -136,6 +135,13 @@ func (s *targetsStore) Write(c context.Context, targets ...*domain.Target) error
}).
F("WHERE id = ?", evt.ID).
Exec(s.db, ctx)
+ case domain.TargetUrlRemoved:
+ return builder.
+ Update("targets", builder.Values{
+ "url": nil,
+ }).
+ F("WHERE id = ?", evt.ID).
+ Exec(s.db, ctx)
case domain.TargetProviderChanged:
return builder.
Update("targets", builder.Values{
diff --git a/pkg/apperr/error_test.go b/pkg/apperr/error_test.go
index 3ae07423..2cf815de 100644
--- a/pkg/apperr/error_test.go
+++ b/pkg/apperr/error_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"github.com/YuukanOO/seelf/pkg/apperr"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
)
func Test_Error(t *testing.T) {
@@ -13,37 +13,37 @@ func Test_Error(t *testing.T) {
msg := "an error !"
err := apperr.New(msg)
- testutil.Equals(t, msg, err.Error())
- testutil.ErrorIs(t, apperr.Error{msg, nil}, err)
- testutil.IsTrue(t, errors.As(err, &apperr.Error{}))
+ assert.Equal(t, msg, err.Error())
+ assert.ErrorIs(t, apperr.Error{msg, nil}, err)
+ assert.True(t, errors.As(err, &apperr.Error{}))
})
t.Run("could be instantiated with a detail error", func(t *testing.T) {
err := errors.New("some infrastructure error")
derr := apperr.NewWithDetail("some_code", err)
- testutil.Equals(t, `some_code:some infrastructure error`, derr.Error())
- testutil.ErrorIs(t, apperr.Error{"some_code", err}, derr)
- testutil.ErrorIs(t, err, derr)
+ assert.Equal(t, `some_code:some infrastructure error`, derr.Error())
+ assert.ErrorIs(t, apperr.Error{"some_code", err}, derr)
+ assert.ErrorIs(t, err, derr)
})
t.Run("implements the Is function for nested errors", func(t *testing.T) {
err := apperr.New("some_pouet")
wrapped := apperr.Wrap(err, errors.New("some infrastructure error"))
- testutil.ErrorIs(t, err, wrapped)
+ assert.ErrorIs(t, err, wrapped)
})
}
func Test_Wrap(t *testing.T) {
- t.Run("should populate the Detail field of a Error", func(t *testing.T) {
+ t.Run("should populate the Detail field of an Error", func(t *testing.T) {
err := apperr.New("some_code")
detail := errors.New("another error")
derr := apperr.Wrap(err, detail)
- testutil.Equals(t, `some_code:another error`, derr.Error())
- testutil.ErrorIs(t, apperr.Error{"some_code", detail}, derr)
+ assert.Equal(t, `some_code:another error`, derr.Error())
+ assert.ErrorIs(t, apperr.Error{"some_code", detail}, derr)
})
t.Run("should create a new Error if err is not one", func(t *testing.T) {
@@ -51,8 +51,8 @@ func Test_Wrap(t *testing.T) {
detail := errors.New("another error")
derr := apperr.Wrap(err, detail)
- testutil.Equals(t, `some_code:another error`, derr.Error())
- testutil.ErrorIs(t, apperr.Error{"some_code", detail}, derr)
+ assert.Equal(t, `some_code:another error`, derr.Error())
+ assert.ErrorIs(t, apperr.Error{"some_code", detail}, derr)
})
}
@@ -62,11 +62,11 @@ func Test_As(t *testing.T) {
appErr, ok := apperr.As[apperr.Error](err)
- testutil.IsTrue(t, ok)
- testutil.Equals(t, "base app error", appErr.Error())
+ assert.True(t, ok)
+ assert.Equal(t, "base app error", appErr.Error())
err = errors.New("another one")
_, ok = apperr.As[apperr.Error](err)
- testutil.IsFalse(t, ok)
+ assert.False(t, ok)
})
}
diff --git a/pkg/assert/assert.go b/pkg/assert/assert.go
new file mode 100644
index 00000000..a2aa2d6e
--- /dev/null
+++ b/pkg/assert/assert.go
@@ -0,0 +1,220 @@
+package assert
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "reflect"
+ "regexp"
+ "testing"
+ "unicode/utf8"
+
+ "github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/event"
+ "github.com/YuukanOO/seelf/pkg/validate"
+)
+
+// Asserts that the given value is true
+func True[T ~bool](t testing.TB, actual T, formatAndMessage ...any) {
+ if actual {
+ return
+ }
+
+ failed(t, "should have been true", true, actual, formatAndMessage)
+}
+
+// Asserts that the given value is false
+func False[T ~bool](t testing.TB, actual T, formatAndMessage ...any) {
+ if !actual {
+ return
+ }
+
+ failed(t, "should have been false", false, actual, formatAndMessage)
+}
+
+// Asserts that the given value is nil
+func Nil(t testing.TB, actual any, formatAndMessage ...any) {
+ if actual == nil {
+ return
+ }
+
+ failed(t, "should have been nil", nil, actual, formatAndMessage)
+}
+
+// Asserts that the given value is not nil
+func NotNil(t testing.TB, actual any, formatAndMessage ...any) {
+ if actual != nil {
+ return
+ }
+
+ failed(t, "should have been not nil", "nothing but ", actual, formatAndMessage)
+}
+
+// Asserts that the given values are equal
+func Equal[T comparable](t testing.TB, expected, actual T, formatAndMessage ...any) {
+ if expected == actual {
+ return
+ }
+
+ failed(t, "should have been equal", expected, actual, formatAndMessage)
+}
+
+// Asserts that the given values are not equal
+func NotEqual[T comparable](t testing.TB, expected, actual T, formatAndMessage ...any) {
+ if expected != actual {
+ return
+ }
+
+ failed(t, "should not have been equal", expected, actual, formatAndMessage)
+}
+
+// Asserts that the given values are deeply equal using the reflect.DeepEqual function
+func DeepEqual[T any](t testing.TB, expected, actual T, formatAndMessage ...any) {
+ if reflect.DeepEqual(expected, actual) {
+ return
+ }
+
+ failed(t, "should have been deeply equal", expected, actual, formatAndMessage)
+}
+
+// Asserts that the given value is of the given type and returns it.
+func Is[T any](t testing.TB, actual any, formatAndMessage ...any) T {
+ result, ok := actual.(T)
+
+ if ok {
+ return result
+ }
+
+ failed(t, "wrong type", reflect.TypeOf(result).String(), reflect.TypeOf(actual).String(), formatAndMessage)
+
+ return result
+}
+
+// Asserts that the given error is the expected error using the function errors.Is
+func ErrorIs(t testing.TB, expected, actual error, formatAndMessage ...any) {
+ if errors.Is(actual, expected) {
+ return
+ }
+
+ failed(t, "errors should have match", expected, actual, formatAndMessage)
+}
+
+// Asserts that the actual slice has the expected length
+func HasLength[T any](t testing.TB, expected int, actual []T, formatAndMessage ...any) {
+ got := len(actual)
+
+ if got == expected {
+ return
+ }
+
+ failed(t, "should have correct length", expected, got, formatAndMessage)
+}
+
+// Asserts that the actual string has the expected number of utf8 runes
+func HasNRunes[T ~string](t testing.TB, expected int, actual T, formatAndMessage ...any) {
+ got := utf8.RuneCountInString(string(actual))
+
+ if got == expected {
+ return
+ }
+
+ failed(t, "should have correct number of characters", expected, got, formatAndMessage)
+}
+
+// Asserts that the actual source has the expected number of events
+func HasNEvents[T event.Source](t testing.TB, expected int, source T, formatAndMessage ...any) {
+ got := len(event.Unwrap(source))
+
+ if got == expected {
+ return
+ }
+
+ failed(t, "should have correct number of events", expected, got, formatAndMessage)
+}
+
+// Asserts that the actual source has the expected event type at the given index and returns it
+func EventIs[T event.Event](t testing.TB, source event.Source, index int, formatAndMessage ...any) T {
+ events := event.Unwrap(source)
+
+ if index >= len(events) {
+ failed(t, "could not find an event at given index", index, len(events), formatAndMessage)
+ var r T
+ return r
+ }
+
+ return Is[T](t, events[index], formatAndMessage...)
+}
+
+// Asserts that the actual error is a validation error with the expected field errors
+func ValidationError(t testing.TB, expected validate.FieldErrors, actual error, formatAndMessage ...any) {
+ ErrorIs(t, validate.ErrValidationFailed, actual, formatAndMessage...)
+
+ fields, ok := apperr.As[validate.FieldErrors](actual)
+
+ if !ok {
+ failed(t, "wrong error type", reflect.TypeOf(expected).String(), reflect.TypeOf(actual).String(), formatAndMessage)
+ return
+ }
+
+ DeepEqual(t, expected, fields, formatAndMessage...)
+}
+
+// Asserts that the given value is the zero value for the corresponding type
+func Zero[T comparable](t testing.TB, actual T, formatAndMessage ...any) T {
+ var zero T
+
+ if actual == zero {
+ return actual
+ }
+
+ failed(t, "should be zero", zero, actual, formatAndMessage)
+
+ return actual
+}
+
+// Asserts that the given value is not the zero value for the corresponding type and returns it
+func NotZero[T comparable](t testing.TB, actual T, formatAndMessage ...any) T {
+ var zero T
+
+ if actual != zero {
+ return actual
+ }
+
+ failed(t, "should not be zero", "anything but the zero value", actual, formatAndMessage)
+
+ return actual
+}
+
+// Asserts that the given value matches the expected regular expression
+func Match(t testing.TB, expectedRegexp string, value string, formatAndMessage ...any) {
+ if regexp.MustCompile(expectedRegexp).MatchString(value) {
+ return
+ }
+
+ failed(t, "should match", expectedRegexp, value, formatAndMessage)
+}
+
+// Asserts that the file at the given path contains the expected content
+func FileContentEquals(t testing.TB, expectedContent string, path string, formatAndMessage ...any) {
+ data, _ := os.ReadFile(path)
+ str := string(data)
+
+ if str == expectedContent {
+ return
+ }
+
+ failed(t, "should contains", expectedContent, str, formatAndMessage)
+}
+
+func failed(t testing.TB, msg string, expected, actual any, contextMessage []any) {
+ if len(contextMessage) > 0 {
+ msg = fmt.Sprintf("%s - %s", msg, fmt.Sprintf(contextMessage[0].(string), contextMessage[1:]...))
+ }
+
+ t.Errorf(`%s
+ expected:
+%#v
+
+ got:
+%#v`, msg, expected, actual)
+}
diff --git a/pkg/assert/assert_test.go b/pkg/assert/assert_test.go
new file mode 100644
index 00000000..20e8b540
--- /dev/null
+++ b/pkg/assert/assert_test.go
@@ -0,0 +1,589 @@
+package assert_test
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/YuukanOO/seelf/pkg/assert"
+ "github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/event"
+ "github.com/YuukanOO/seelf/pkg/validate"
+ "github.com/YuukanOO/seelf/pkg/validate/numbers"
+ "github.com/YuukanOO/seelf/pkg/validate/strings"
+)
+
+func Test_True(t *testing.T) {
+ t.Run("should correctly fail given a false value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.True(mock, false, "with value %s", "false")
+
+ shouldHaveFailed(t, mock, `should have been true - with value false
+ expected:
+true
+
+ got:
+false`)
+ })
+
+ t.Run("should correctly pass given a true value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.True(mock, true, "with value %s", "true")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_False(t *testing.T) {
+ t.Run("should correctly fail given a true value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.False(mock, true, "with value %s", "true")
+
+ shouldHaveFailed(t, mock, `should have been false - with value true
+ expected:
+false
+
+ got:
+true`)
+ })
+
+ t.Run("should correctly pass given a false value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.False(mock, false, "with value %s", "false")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_Nil(t *testing.T) {
+ t.Run("should correctly fail given a non nil value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.Nil(mock, "a string", "with a non nil value")
+
+ shouldHaveFailed(t, mock, `should have been nil - with a non nil value
+ expected:
+
+
+ got:
+"a string"`)
+ })
+
+ t.Run("should correctly pass given a nil value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.Nil(mock, nil, "with a nil value")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_NotNil(t *testing.T) {
+ t.Run("should correctly fail given a nil value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.NotNil(mock, nil, "with a nil value")
+
+ shouldHaveFailed(t, mock, `should have been not nil - with a nil value
+ expected:
+"nothing but "
+
+ got:
+`)
+ })
+
+ t.Run("should correctly pass given a non nil value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.NotNil(mock, "a string", "with a non nil value")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_Equal(t *testing.T) {
+ t.Run("should correctly fail given different values", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.Equal(mock, true, false, "with different values")
+
+ shouldHaveFailed(t, mock, `should have been equal - with different values
+ expected:
+true
+
+ got:
+false`)
+ })
+
+ t.Run("should correctly pass given the expected value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.Equal(mock, true, true, "with same values")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_NotEqual(t *testing.T) {
+ t.Run("should correctly fail given the expected value", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.NotEqual(mock, true, true, "with same values")
+
+ shouldHaveFailed(t, mock, `should not have been equal - with same values
+ expected:
+true
+
+ got:
+true`)
+ })
+
+ t.Run("should correctly pass given different values", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.NotEqual(mock, true, false, "with different values")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_DeepEqual(t *testing.T) {
+ t.Run("should correctly fail given different slices", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.DeepEqual(mock, []int{1}, []int{2}, "with different slices")
+
+ shouldHaveFailed(t, mock, `should have been deeply equal - with different slices
+ expected:
+[]int{1}
+
+ got:
+[]int{2}`)
+ })
+
+ t.Run("should correctly pass given the same slice", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.DeepEqual(mock, []int{1}, []int{1}, "with the same slice")
+
+ shouldHaveSucceeded(t, mock)
+ })
+
+ t.Run("should correctly pass given the same struct", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.DeepEqual(mock, struct {
+ foo string
+ bar int
+ }{foo: "bar", bar: 42}, struct {
+ foo string
+ bar int
+ }{foo: "bar", bar: 42}, "with the same struct")
+
+ shouldHaveSucceeded(t, mock)
+ })
+
+ t.Run("should correctly fail given different structs", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.DeepEqual(mock, struct {
+ foo string
+ bar int
+ }{foo: "bar", bar: 42}, struct {
+ foo string
+ bar int
+ }{foo: "bar", bar: 24}, "with different structs")
+
+ shouldHaveFailed(t, mock, `should have been deeply equal - with different structs
+ expected:
+struct { foo string; bar int }{foo:"bar", bar:42}
+
+ got:
+struct { foo string; bar int }{foo:"bar", bar:24}`)
+ })
+}
+
+func Test_Is(t *testing.T) {
+ t.Run("should correctly fail given the wrong type", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.Is[string](mock, 5, "with wrong type")
+
+ shouldHaveFailed(t, mock, `wrong type - with wrong type
+ expected:
+"string"
+
+ got:
+"int"`)
+
+ if result != "" {
+ t.Error("result should be empty")
+ }
+ })
+
+ t.Run("should correctly pass given the right type", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.Is[string](mock, "test", "with right type")
+
+ shouldHaveSucceeded(t, mock)
+
+ if result != "test" {
+ t.Error("result should be 'test'")
+ }
+ })
+}
+
+func Test_ErrorIs(t *testing.T) {
+ t.Run("should correctly fail given a wrong error", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.ErrorIs(mock, errors.New("test"), errors.New("another err"), "with wrong error")
+
+ shouldHaveFailed(t, mock, `errors should have match - with wrong error
+ expected:
+&errors.errorString{s:"test"}
+
+ got:
+&errors.errorString{s:"another err"}`)
+ })
+
+ t.Run("should correctly pass given a right error", func(t *testing.T) {
+ mock := new(mockT)
+ expectedErr := errors.New("test")
+ actualErr := fmt.Errorf("with wrapped error %w", expectedErr)
+
+ assert.ErrorIs(mock, expectedErr, actualErr, "with right error")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_HasLength(t *testing.T) {
+ t.Run("should correctly fail given a wrong length", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.HasLength(mock, 5, []int{1, 2, 3}, "with wrong length")
+
+ shouldHaveFailed(t, mock, `should have correct length - with wrong length
+ expected:
+5
+
+ got:
+3`)
+ })
+
+ t.Run("should correctly pass given a right length", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.HasLength(mock, 3, []int{1, 2, 3}, "with right length")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_HasNRunes(t *testing.T) {
+ t.Run("should correctly fail given a wrong length", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.HasNRunes(mock, 5, "test", "with wrong length")
+
+ shouldHaveFailed(t, mock, `should have correct number of characters - with wrong length
+ expected:
+5
+
+ got:
+4`)
+ })
+
+ t.Run("should correctly pass given a right length", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.HasNRunes(mock, 4, "test", "with right length")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+type (
+ eventA struct {
+ bus.Notification
+ value string
+ }
+
+ eventB struct {
+ bus.Notification
+ value int
+ }
+
+ entity struct {
+ event.Emitter
+ }
+)
+
+func (event eventA) Name_() string { return "eventA" }
+func (event eventB) Name_() string { return "eventB" }
+
+func Test_HasNEvents(t *testing.T) {
+ ent := entity{}
+ event.Store(&ent, eventA{}, eventB{})
+
+ t.Run("should correctly fail given a wrong length", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.HasNEvents(mock, 1, &ent, "with wrong length")
+
+ shouldHaveFailed(t, mock, `should have correct number of events - with wrong length
+ expected:
+1
+
+ got:
+2`)
+ })
+
+ t.Run("should correctly pass given a right length", func(t *testing.T) {
+ mock := new(mockT)
+
+ assert.HasNEvents(mock, 2, &ent, "with right length")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_EventIs(t *testing.T) {
+ ent := entity{}
+ a := eventA{value: "value"}
+ b := eventB{value: 42}
+ event.Store(&ent, a, b)
+
+ t.Run("should fail if index is out of range", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.EventIs[eventA](mock, &ent, 2, "with wrong length")
+
+ shouldHaveFailed(t, mock, `could not find an event at given index - with wrong length
+ expected:
+2
+
+ got:
+2`)
+
+ if result == a {
+ t.Error("result should be empty")
+ }
+ })
+
+ t.Run("should fail if requested event type is wrong", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.EventIs[eventB](mock, &ent, 0, "with wrong event type")
+
+ shouldHaveFailed(t, mock, `wrong type - with wrong event type
+ expected:
+"assert_test.eventB"
+
+ got:
+"assert_test.eventA"`)
+
+ if result == b {
+ t.Error("result should be empty")
+ }
+ })
+
+ t.Run("should pass if requested event type is right", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.EventIs[eventA](mock, &ent, 0, "with right event type")
+
+ shouldHaveSucceeded(t, mock)
+
+ if result != a {
+ t.Error("result should be equal to a")
+ }
+ })
+}
+
+func Test_ValidationError(t *testing.T) {
+ t.Run("should fail if the error is not a validation one", func(t *testing.T) {
+ mock := new(mockT)
+ err := errors.New("test")
+
+ assert.ValidationError(mock, validate.FieldErrors{}, err, "with wrong error type")
+
+ shouldHaveFailed(t, mock, `wrong error type - with wrong error type
+ expected:
+"validate.FieldErrors"
+
+ got:
+"*errors.errorString"`)
+ })
+
+ t.Run("should fail if FieldErrors do not match", func(t *testing.T) {
+ mock := new(mockT)
+ err := validate.NewError(validate.FieldErrors{
+ "a": numbers.ErrMin,
+ "b": strings.ErrRequired,
+ })
+
+ assert.ValidationError(mock, validate.FieldErrors{
+ "a": strings.ErrRequired,
+ "b": numbers.ErrMin,
+ }, err, "with wrong FieldErrors")
+
+ shouldHaveFailed(t, mock, `should have been deeply equal - with wrong FieldErrors
+ expected:
+validate.FieldErrors{"a":apperr.Error{Code:"required", Detail:error(nil)}, "b":apperr.Error{Code:"min", Detail:error(nil)}}
+
+ got:
+validate.FieldErrors{"a":apperr.Error{Code:"min", Detail:error(nil)}, "b":apperr.Error{Code:"required", Detail:error(nil)}}`)
+ })
+
+ t.Run("should pass if FieldErrors match", func(t *testing.T) {
+ mock := new(mockT)
+ err := validate.NewError(validate.FieldErrors{
+ "a": numbers.ErrMin,
+ "b": strings.ErrRequired,
+ })
+
+ assert.ValidationError(mock, validate.FieldErrors{
+ "a": numbers.ErrMin,
+ "b": strings.ErrRequired,
+ }, err, "with right FieldErrors")
+
+ shouldHaveSucceeded(t, mock)
+ })
+}
+
+func Test_Zero(t *testing.T) {
+ t.Run("should fail if the value is not the default one", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.Zero(mock, "test", "with a string")
+
+ shouldHaveFailed(t, mock, `should be zero - with a string
+ expected:
+""
+
+ got:
+"test"`)
+
+ if result != "test" {
+ t.Error("result should be equal to the given value")
+ }
+ })
+
+ t.Run("should pass if the value is the default one", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.Zero(mock, "", "with an empty string")
+
+ shouldHaveSucceeded(t, mock)
+
+ if result != "" {
+ t.Error("result should be empty")
+ }
+ })
+}
+
+func Test_NotZero(t *testing.T) {
+ t.Run("should fail if the value is the default one for simple types", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.NotZero(mock, "", "with an empty string")
+
+ shouldHaveFailed(t, mock, `should not be zero - with an empty string
+ expected:
+"anything but the zero value"
+
+ got:
+""`)
+
+ if result != "" {
+ t.Error("result should be empty")
+ }
+ })
+
+ t.Run("should pass if the value is not the default one for simple types", func(t *testing.T) {
+ mock := new(mockT)
+
+ result := assert.NotZero(mock, "test", "with a string")
+
+ shouldHaveSucceeded(t, mock)
+
+ if result != "test" {
+ t.Error("result should be equal to the given value")
+ }
+ })
+
+ t.Run("should fail if the value is the default one for complex types", func(t *testing.T) {
+ mock := new(mockT)
+ var time time.Time
+
+ result := assert.NotZero(mock, time, "with a time.Time value")
+
+ shouldHaveFailed(t, mock, `should not be zero - with a time.Time value
+ expected:
+"anything but the zero value"
+
+ got:
+time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)`)
+
+ if result != time {
+ t.Error("result should be empty")
+ }
+ })
+
+ t.Run("should pass if the value is not the default one for complex types", func(t *testing.T) {
+ mock := new(mockT)
+ time := time.Now().UTC()
+
+ result := assert.NotZero(mock, time, "with a time.Time value")
+
+ shouldHaveSucceeded(t, mock)
+
+ if result != time {
+ t.Error("result should be equal to the given value")
+ }
+ })
+}
+
+type mockT struct {
+ testing.TB
+ hasFailed bool
+ msg string
+}
+
+func (t *mockT) Errorf(format string, args ...any) {
+ t.hasFailed = true
+ t.msg = fmt.Sprintf(format, args...)
+}
+
+func shouldHaveFailed(t testing.TB, mock *mockT, expectedMessage string) {
+ if !mock.hasFailed {
+ t.Error("should have failed")
+ }
+
+ if mock.msg != expectedMessage {
+ t.Errorf(`message should have matched:
+expected:
+ %s
+
+got:
+ %s`, expectedMessage, mock.msg)
+ }
+}
+
+func shouldHaveSucceeded(t testing.TB, mock *mockT) {
+ if mock.hasFailed {
+ t.Error("should not have failed")
+ }
+
+ if mock.msg != "" {
+ t.Error("message should be empty")
+ }
+}
diff --git a/pkg/bus/dispatcher.go b/pkg/bus/dispatcher.go
index ed1d876c..4cf2d7fb 100644
--- a/pkg/bus/dispatcher.go
+++ b/pkg/bus/dispatcher.go
@@ -5,6 +5,7 @@ import (
"errors"
"github.com/YuukanOO/seelf/pkg/storage"
+ "github.com/YuukanOO/seelf/pkg/types"
)
var ErrNoHandlerRegistered = errors.New("no_handler_registered")
@@ -30,7 +31,10 @@ type (
}
)
-// Register an handler for a specific request on the provided bus.
+// Register an handler for a specific request on the provided bus. You should always
+// prefer this registration method.
+// If the provided message is an async one, it will be automatically registered on
+// the Marshallable mapper to make things easier.
func Register[TResult any, TMsg TypedRequest[TResult]](bus Bus, handler RequestHandler[TResult, TMsg]) {
var (
msg TMsg
@@ -42,8 +46,12 @@ func Register[TResult any, TMsg TypedRequest[TResult]](bus Bus, handler RequestH
bus.Register(msg, h)
// If the message is schedulable, register the unmarshaller automatically.
- if _, isSchedulable := any(msg).(Schedulable); isSchedulable {
- Marshallable.Register(msg, func(s string) (Request, error) { return storage.UnmarshalJSON[TMsg](s) })
+ // This is done here because of the known type TMsg but maybe I should try to
+ // move it to bus/memory in the future.
+ if types.Is[Schedulable](msg) {
+ Marshallable.Register(msg, func(s string) (Request, error) {
+ return storage.UnmarshalJSON[TMsg](s)
+ })
}
}
diff --git a/pkg/bus/memory/dispatcher.go b/pkg/bus/memory/dispatcher.go
index a4beac75..64c58197 100644
--- a/pkg/bus/memory/dispatcher.go
+++ b/pkg/bus/memory/dispatcher.go
@@ -4,6 +4,7 @@ import (
"context"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/types"
)
type (
@@ -28,12 +29,12 @@ func (b *dispatcher) Register(msg bus.Message, handler bus.NextFunc) {
name := msg.Name_()
_, exists := b.handlers[name]
- // Apply middlewares to avoid doing it at runtime
+ // Apply middlewares here to avoid doing it at runtime
for i := len(b.middlewares) - 1; i >= 0; i-- {
handler = b.middlewares[i](handler)
}
- if msg.Kind_() == bus.MessageKindNotification {
+ if types.Is[bus.Signal](msg) {
if !exists {
b.handlers[name] = []bus.NextFunc{handler}
} else {
@@ -61,15 +62,15 @@ func (b *dispatcher) Send(ctx context.Context, msg bus.Request) (any, error) {
func (b *dispatcher) Notify(ctx context.Context, msgs ...bus.Signal) error {
for _, msg := range msgs {
- handlers := b.handlers[msg.Name_()]
+ value := b.handlers[msg.Name_()]
- if handlers == nil {
+ if value == nil {
continue
}
- hdls := handlers.([]bus.NextFunc)
+ handlers := value.([]bus.NextFunc)
- for _, h := range hdls {
+ for _, h := range handlers {
_, err := h(ctx, msg)
if err != nil {
diff --git a/pkg/bus/memory/dispatcher_test.go b/pkg/bus/memory/dispatcher_test.go
index c94fa111..b7bbb02b 100644
--- a/pkg/bus/memory/dispatcher_test.go
+++ b/pkg/bus/memory/dispatcher_test.go
@@ -5,9 +5,9 @@ import (
"errors"
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
"github.com/YuukanOO/seelf/pkg/bus/memory"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func TestBus(t *testing.T) {
@@ -38,7 +38,7 @@ func TestBus(t *testing.T) {
_, err := bus.Send(local, context.Background(), &addCommand{})
- testutil.ErrorIs(t, bus.ErrNoHandlerRegistered, err)
+ assert.ErrorIs(t, bus.ErrNoHandlerRegistered, err)
})
t.Run("should returns the request handler error back if any", func(t *testing.T) {
@@ -51,7 +51,7 @@ func TestBus(t *testing.T) {
_, err := bus.Send(local, context.Background(), addCommand{})
- testutil.ErrorIs(t, expectedErr, err)
+ assert.ErrorIs(t, expectedErr, err)
})
t.Run("should call the appropriate request handler and returns the result", func(t *testing.T) {
@@ -64,13 +64,13 @@ func TestBus(t *testing.T) {
result, err := bus.Send(local, context.Background(), addCommand{A: 1, B: 2})
- testutil.IsNil(t, err)
- testutil.Equals(t, 3, result)
+ assert.Nil(t, err)
+ assert.Equal(t, 3, result)
result, err = bus.Send(local, context.Background(), getQuery{})
- testutil.IsNil(t, err)
- testutil.Equals(t, 42, result)
+ assert.Nil(t, err)
+ assert.Equal(t, 42, result)
})
t.Run("should do nothing if no signal handler is registered for a given signal", func(t *testing.T) {
@@ -78,7 +78,7 @@ func TestBus(t *testing.T) {
err := local.Notify(context.Background(), registeredNotification{})
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
})
t.Run("should returns a signal handler error back if any", func(t *testing.T) {
@@ -95,7 +95,7 @@ func TestBus(t *testing.T) {
err := local.Notify(context.Background(), registeredNotification{})
- testutil.ErrorIs(t, expectedErr, err)
+ assert.ErrorIs(t, expectedErr, err)
})
t.Run("should call every signal handlers registered for the given signal", func(t *testing.T) {
@@ -117,8 +117,8 @@ func TestBus(t *testing.T) {
err := local.Notify(context.Background(), registeredNotification{})
- testutil.IsNil(t, err)
- testutil.IsTrue(t, firstOneCalled && secondOneCalled)
+ assert.Nil(t, err)
+ assert.True(t, firstOneCalled && secondOneCalled)
})
t.Run("should call every middlewares registered", func(t *testing.T) {
@@ -153,16 +153,16 @@ func TestBus(t *testing.T) {
B: 2,
})
- testutil.IsNil(t, err)
- testutil.Equals(t, 3, r)
- testutil.DeepEquals(t, []int{1, 2, 2, 1}, calls)
+ assert.Nil(t, err)
+ assert.Equal(t, 3, r)
+ assert.DeepEqual(t, []int{1, 2, 2, 1}, calls)
calls = make([]int, 0)
- local.Notify(context.Background(), registeredNotification{})
+ assert.Nil(t, local.Notify(context.Background(), registeredNotification{}))
// Should have been called twice cuz 2 signal handlers are registered
- testutil.DeepEquals(t, []int{1, 2, 2, 1, 1, 2, 2, 1}, calls)
+ assert.DeepEqual(t, []int{1, 2, 2, 1, 1, 2, 2, 1}, calls)
})
}
diff --git a/pkg/bus/message_test.go b/pkg/bus/message_test.go
index fa639ec4..a445d079 100644
--- a/pkg/bus/message_test.go
+++ b/pkg/bus/message_test.go
@@ -3,8 +3,8 @@ package bus_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func TestMessage(t *testing.T) {
@@ -15,9 +15,9 @@ func TestMessage(t *testing.T) {
notif registeredNotification
)
- testutil.Equals(t, bus.MessageKindCommand, command.Kind_())
- testutil.Equals(t, bus.MessageKindQuery, query.Kind_())
- testutil.Equals(t, bus.MessageKindNotification, notif.Kind_())
+ assert.Equal(t, bus.MessageKindCommand, command.Kind_())
+ assert.Equal(t, bus.MessageKindQuery, query.Kind_())
+ assert.Equal(t, bus.MessageKindNotification, notif.Kind_())
})
}
diff --git a/pkg/bus/scheduler.go b/pkg/bus/scheduler.go
index 954b1358..f37d3180 100644
--- a/pkg/bus/scheduler.go
+++ b/pkg/bus/scheduler.go
@@ -10,8 +10,6 @@ import (
"github.com/YuukanOO/seelf/pkg/storage"
)
-var _ Scheduler = (*defaultScheduler)(nil) // Validate interface implementation
-
const (
JobPolicyRetryPreserveOrder JobPolicy = 1 << iota // Retry the job but preserve the order among the group
JobPolicyWaitForOthersResourceID // Wait for other jobs on the same resource id to finish before processing
diff --git a/pkg/bus/scheduler_test.go b/pkg/bus/scheduler_test.go
index cc161a67..ec5ed897 100644
--- a/pkg/bus/scheduler_test.go
+++ b/pkg/bus/scheduler_test.go
@@ -8,13 +8,13 @@ import (
"sync"
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
"github.com/YuukanOO/seelf/pkg/bus/memory"
"github.com/YuukanOO/seelf/pkg/flag"
"github.com/YuukanOO/seelf/pkg/log"
"github.com/YuukanOO/seelf/pkg/must"
"github.com/YuukanOO/seelf/pkg/storage"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func TestScheduler(t *testing.T) {
@@ -41,35 +41,35 @@ func TestScheduler(t *testing.T) {
withUnwrapedErr := returnCommand{err: innerErr}
withPreservedOrderErr := returnCommand{err: innerErr}
- testutil.IsNil(t, scheduler.Queue(context.Background(), withoutErr))
- testutil.IsNil(t, scheduler.Queue(context.Background(), withUnwrapedErr))
- testutil.IsNil(t, scheduler.Queue(context.Background(), withPreservedOrderErr, bus.WithPolicy(bus.JobPolicyRetryPreserveOrder)))
- testutil.IsNil(t, scheduler.Queue(context.Background(), addCommand{}))
+ assert.Nil(t, scheduler.Queue(context.Background(), withoutErr))
+ assert.Nil(t, scheduler.Queue(context.Background(), withUnwrapedErr))
+ assert.Nil(t, scheduler.Queue(context.Background(), withPreservedOrderErr, bus.WithPolicy(bus.JobPolicyRetryPreserveOrder)))
+ assert.Nil(t, scheduler.Queue(context.Background(), addCommand{}))
adapter.wait()
- testutil.HasLength(t, adapter.done, 1)
+ assert.HasLength(t, 1, adapter.done)
slices.SortFunc(adapter.done, func(a, b *job) int {
return a.id - b.id
})
- testutil.Equals(t, 0, adapter.done[0].id)
+ assert.Equal(t, 0, adapter.done[0].id)
- testutil.HasLength(t, adapter.retried, 3)
+ assert.HasLength(t, 3, adapter.retried)
slices.SortFunc(adapter.retried, func(a, b *job) int {
return a.id - b.id
})
- testutil.Equals(t, 1, adapter.retried[0].id)
- testutil.ErrorIs(t, innerErr, adapter.retried[0].err)
- testutil.IsFalse(t, adapter.retried[0].preserveOrder)
+ assert.Equal(t, 1, adapter.retried[0].id)
+ assert.ErrorIs(t, innerErr, adapter.retried[0].err)
+ assert.False(t, adapter.retried[0].preserveOrder)
- testutil.Equals(t, 2, adapter.retried[1].id)
- testutil.ErrorIs(t, innerErr, adapter.retried[1].err)
- testutil.IsTrue(t, adapter.retried[1].preserveOrder)
+ assert.Equal(t, 2, adapter.retried[1].id)
+ assert.ErrorIs(t, innerErr, adapter.retried[1].err)
+ assert.True(t, adapter.retried[1].preserveOrder)
- testutil.Equals(t, 3, adapter.retried[2].id)
- testutil.ErrorIs(t, bus.ErrNoHandlerRegistered, adapter.retried[2].err)
+ assert.Equal(t, 3, adapter.retried[2].id)
+ assert.ErrorIs(t, bus.ErrNoHandlerRegistered, adapter.retried[2].err)
})
}
diff --git a/pkg/bus/spy/dispatcher.go b/pkg/bus/spy/dispatcher.go
new file mode 100644
index 00000000..75764fb2
--- /dev/null
+++ b/pkg/bus/spy/dispatcher.go
@@ -0,0 +1,48 @@
+//go:build !release
+
+package spy
+
+import (
+ "context"
+
+ "github.com/YuukanOO/seelf/pkg/bus"
+)
+
+type (
+ Dispatcher interface {
+ bus.Dispatcher
+
+ Reset() // Clear all requests and signals
+ Requests() []bus.Request
+ Signals() []bus.Signal
+ }
+
+ dispatcher struct {
+ requests []bus.Request
+ signals []bus.Signal
+ }
+)
+
+// Builds a new dispatcher used for testing only. It will not send anything but
+// append the requests and signals to the internal slices so they can be checked.
+func NewDispatcher() Dispatcher {
+ return &dispatcher{}
+}
+
+func (d *dispatcher) Send(ctx context.Context, msg bus.Request) (any, error) {
+ d.requests = append(d.requests, msg)
+ return nil, nil
+}
+
+func (d *dispatcher) Notify(ctx context.Context, msgs ...bus.Signal) error {
+ d.signals = append(d.signals, msgs...)
+ return nil
+}
+
+func (d *dispatcher) Reset() {
+ d.requests = nil
+ d.signals = nil
+}
+
+func (d *dispatcher) Requests() []bus.Request { return d.requests }
+func (d *dispatcher) Signals() []bus.Signal { return d.signals }
diff --git a/pkg/bus/sqlite/store.go b/pkg/bus/sqlite/store.go
index 020b76f2..d55c21ba 100644
--- a/pkg/bus/sqlite/store.go
+++ b/pkg/bus/sqlite/store.go
@@ -2,9 +2,7 @@ package sqlite
import (
"context"
- "database/sql"
"embed"
- "errors"
"time"
"github.com/YuukanOO/seelf/pkg/apperr"
@@ -21,7 +19,7 @@ var (
//go:embed migrations/*.sql
migrations embed.FS
- migrationsModule = sqlite.NewMigrationsModule("scheduler", "migrations", migrations)
+ Migrations = sqlite.NewMigrationsModule("scheduler", "migrations", migrations)
)
type (
@@ -67,7 +65,7 @@ func NewScheduledJobsStore(db *sqlite.Database) bus.ScheduledJobsStore {
// them as not retrieved so they will be picked up next time GetNextPendingJobs is called.
// You MUST call this method at the application startup.
func (s *store) Setup() error {
- if err := s.db.Migrate(migrationsModule); err != nil {
+ if err := s.db.Migrate(Migrations); err != nil {
return err
}
@@ -99,18 +97,16 @@ func (s *store) Create(
// Could not use the ON CONFLICT here :'(
if flag.IsSet(options.Policy, bus.JobPolicyMerge) {
- var existingJobId string
-
- if err = s.db.QueryRowContext(ctx, `
- SELECT id
- FROM scheduled_jobs
- WHERE resource_id = ? AND message_name = ? AND retrieved = false`, resourceId, msgName).
- Scan(&existingJobId); err != nil && !errors.Is(err, sql.ErrNoRows) {
- return err
- }
-
- if existingJobId != "" {
- _, err = s.db.ExecContext(ctx, `UPDATE scheduled_jobs SET message_data = ? WHERE id = ?`, msgValue, existingJobId)
+ result, err := s.db.ExecContext(ctx, `
+ UPDATE scheduled_jobs
+ SET message_data = ?
+ WHERE id = (
+ SELECT id
+ FROM scheduled_jobs
+ WHERE resource_id = ? AND message_name = ? AND retrieved = false
+ )`, msgValue, resourceId, msgName)
+
+ if affected, _ := result.RowsAffected(); affected > 0 {
return err
}
}
diff --git a/pkg/config/loader_test.go b/pkg/config/loader_test.go
index 7f1d5d45..d9ea5fe9 100644
--- a/pkg/config/loader_test.go
+++ b/pkg/config/loader_test.go
@@ -6,10 +6,10 @@ import (
"os"
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/config"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/ostools"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
type (
@@ -149,20 +149,20 @@ HTTP_TWO=true`,
if tt.conf != "" {
err := ostools.WriteFile(confFilename, []byte(tt.conf))
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
}
if tt.env != "" {
err := ostools.WriteFile(envFilename, []byte(tt.env))
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
}
var conf configuration
exists, err := config.Load(confFilename, &conf, envFilename)
- testutil.IsNil(t, err)
- testutil.Equals(t, tt.conf != "", exists)
- testutil.DeepEquals(t, tt.expected, conf)
+ assert.Nil(t, err)
+ assert.Equal(t, tt.conf != "", exists)
+ assert.DeepEqual(t, tt.expected, conf)
})
}
@@ -174,8 +174,8 @@ HTTP_TWO=true`,
exists, err := config.Load(confFilename, &conf)
- testutil.ErrorIs(t, errPostLoad, err)
- testutil.IsFalse(t, exists)
+ assert.ErrorIs(t, errPostLoad, err)
+ assert.False(t, exists)
})
}
@@ -203,10 +203,10 @@ func Test_Save(t *testing.T) {
err := config.Save(confFilename, conf)
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
b, err := os.ReadFile(confFilename)
- testutil.IsNil(t, err)
- testutil.Equals(t, `verbose: true
+ assert.Nil(t, err)
+ assert.Equal(t, `verbose: true
http:
host: 127.0.0.1
secure: true
diff --git a/pkg/crypto/random_test.go b/pkg/crypto/random_test.go
index cc19d918..bb105f66 100644
--- a/pkg/crypto/random_test.go
+++ b/pkg/crypto/random_test.go
@@ -3,12 +3,12 @@ package crypto_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/crypto"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_RandomKey(t *testing.T) {
key, err := crypto.RandomKey[string](32)
- testutil.IsNil(t, err)
- testutil.HasNChars(t, 32, key)
+ assert.Nil(t, err)
+ assert.HasNRunes(t, 32, key)
}
diff --git a/pkg/domain/action_test.go b/pkg/domain/action_test.go
index fdb6da3b..3805a432 100644
--- a/pkg/domain/action_test.go
+++ b/pkg/domain/action_test.go
@@ -4,9 +4,9 @@ import (
"testing"
"time"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/domain"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
type userId string
@@ -16,8 +16,8 @@ func Test_Action(t *testing.T) {
var user userId = "john"
act := domain.NewAction(user)
- testutil.Equals(t, user, act.By())
- testutil.IsFalse(t, act.At().IsZero())
+ assert.Equal(t, user, act.By())
+ assert.False(t, act.At().IsZero())
})
t.Run("should be rehydrated with the From function", func(t *testing.T) {
@@ -28,7 +28,7 @@ func Test_Action(t *testing.T) {
act := domain.ActionFrom(user, at)
- testutil.Equals(t, user, act.By())
- testutil.Equals(t, at, act.At())
+ assert.Equal(t, user, act.By())
+ assert.Equal(t, at, act.At())
})
}
diff --git a/pkg/domain/interval_test.go b/pkg/domain/interval_test.go
index dff2693f..0b8c4d1d 100644
--- a/pkg/domain/interval_test.go
+++ b/pkg/domain/interval_test.go
@@ -4,15 +4,15 @@ import (
"testing"
"time"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/domain"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_TimeInterval(t *testing.T) {
t.Run("should fail if the from date is after the to date", func(t *testing.T) {
_, err := domain.NewTimeInterval(time.Now(), time.Now().Add(-time.Second))
- testutil.ErrorIs(t, domain.ErrInvalidTimeInterval, err)
+ assert.ErrorIs(t, domain.ErrInvalidTimeInterval, err)
})
t.Run("should succeed if the from date is before the to date", func(t *testing.T) {
@@ -20,8 +20,8 @@ func Test_TimeInterval(t *testing.T) {
to := time.Now().Add(time.Second)
ti, err := domain.NewTimeInterval(from, to)
- testutil.IsNil(t, err)
- testutil.Equals(t, from, ti.From())
- testutil.Equals(t, to, ti.To())
+ assert.Nil(t, err)
+ assert.Equal(t, from, ti.From())
+ assert.Equal(t, to, ti.To())
})
}
diff --git a/pkg/event/event_test.go b/pkg/event/event_test.go
index e42bc530..1bb9aa27 100644
--- a/pkg/event/event_test.go
+++ b/pkg/event/event_test.go
@@ -3,9 +3,9 @@ package event_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/bus"
"github.com/YuukanOO/seelf/pkg/event"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
type (
@@ -34,9 +34,9 @@ func Test_Emitter(t *testing.T) {
evts := event.Unwrap(&ent)
- testutil.HasLength(t, evts, 2)
- testutil.Equals(t, evt1, evts[0].(domainEventA))
- testutil.Equals(t, evt2, evts[1].(domainEventB))
+ assert.HasLength(t, 2, evts)
+ assert.Equal(t, evt1, evts[0].(domainEventA))
+ assert.Equal(t, evt2, evts[1].(domainEventB))
})
// t.Run("should be able to clear all events from an Emitter", func(t *testing.T) {
diff --git a/pkg/flag/flag_test.go b/pkg/flag/flag_test.go
index 54a9f4c3..95a1e633 100644
--- a/pkg/flag/flag_test.go
+++ b/pkg/flag/flag_test.go
@@ -3,8 +3,8 @@ package flag_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/flag"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
type flagType uint
@@ -16,11 +16,11 @@ const (
)
func Test_IsSet(t *testing.T) {
- testutil.IsTrue(t, flag.IsSet(flagA, flagA))
- testutil.IsFalse(t, flag.IsSet(flagA, flagB))
- testutil.IsTrue(t, flag.IsSet(flagA|flagB, flagA))
- testutil.IsTrue(t, flag.IsSet(flagA|flagB, flagB|flagA))
- testutil.IsTrue(t, flag.IsSet(flagA|flagB|flagC, flagB|flagA))
- testutil.IsFalse(t, flag.IsSet(flagA, flagB|flagA))
- testutil.IsFalse(t, flag.IsSet(flagA|flagC, flagB|flagA))
+ assert.True(t, flag.IsSet(flagA, flagA))
+ assert.False(t, flag.IsSet(flagA, flagB))
+ assert.True(t, flag.IsSet(flagA|flagB, flagA))
+ assert.True(t, flag.IsSet(flagA|flagB, flagB|flagA))
+ assert.True(t, flag.IsSet(flagA|flagB|flagC, flagB|flagA))
+ assert.False(t, flag.IsSet(flagA, flagB|flagA))
+ assert.False(t, flag.IsSet(flagA|flagC, flagB|flagA))
}
diff --git a/pkg/id/id_test.go b/pkg/id/id_test.go
index 678a513e..6c773159 100644
--- a/pkg/id/id_test.go
+++ b/pkg/id/id_test.go
@@ -3,8 +3,8 @@ package id_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/id"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
type someDomainID string
@@ -13,7 +13,7 @@ func Test_ID_GeneratesANonEmptyUniqueIdentifier(t *testing.T) {
id1 := id.New[someDomainID]()
id2 := id.New[someDomainID]()
- testutil.HasNChars(t, 27, id1)
- testutil.HasNChars(t, 27, id2)
- testutil.NotEquals(t, id1, id2)
+ assert.HasNRunes(t, 27, id1)
+ assert.HasNRunes(t, 27, id2)
+ assert.NotEqual(t, id1, id2)
}
diff --git a/pkg/monad/maybe_test.go b/pkg/monad/maybe_test.go
index 1e591d9b..bc295f10 100644
--- a/pkg/monad/maybe_test.go
+++ b/pkg/monad/maybe_test.go
@@ -4,8 +4,8 @@ import (
"testing"
"time"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/monad"
- "github.com/YuukanOO/seelf/pkg/testutil"
"gopkg.in/yaml.v3"
)
@@ -13,19 +13,19 @@ func Test_Maybe(t *testing.T) {
t.Run("should have a default state without value", func(t *testing.T) {
var m monad.Maybe[time.Time]
- testutil.IsFalse(t, m.HasValue())
+ assert.False(t, m.HasValue())
})
t.Run("could be created empty", func(t *testing.T) {
m := monad.None[time.Time]()
- testutil.IsFalse(t, m.HasValue())
+ assert.False(t, m.HasValue())
})
t.Run("could be created with a defined value", func(t *testing.T) {
m := monad.Value("ok")
- testutil.Equals(t, "ok", m.MustGet())
- testutil.IsTrue(t, m.HasValue())
+ assert.Equal(t, "ok", m.MustGet())
+ assert.True(t, m.HasValue())
})
t.Run("could returns its internal value and a boolean indicating if it has been set", func(t *testing.T) {
@@ -33,15 +33,15 @@ func Test_Maybe(t *testing.T) {
value, hasValue := m.TryGet()
- testutil.IsFalse(t, hasValue)
- testutil.Equals(t, "", value)
+ assert.False(t, hasValue)
+ assert.Equal(t, "", value)
m.Set("ok")
value, hasValue = m.TryGet()
- testutil.IsTrue(t, hasValue)
- testutil.Equals(t, "ok", value)
+ assert.True(t, hasValue)
+ assert.Equal(t, "ok", value)
})
t.Run("could be assigned a value", func(t *testing.T) {
@@ -51,8 +51,8 @@ func Test_Maybe(t *testing.T) {
)
m.Set(now)
- testutil.Equals(t, now, m.MustGet())
- testutil.IsTrue(t, m.HasValue())
+ assert.Equal(t, now, m.MustGet())
+ assert.True(t, m.HasValue())
})
t.Run("could unset its value", func(t *testing.T) {
@@ -60,14 +60,14 @@ func Test_Maybe(t *testing.T) {
m.Unset()
- testutil.IsFalse(t, m.HasValue())
+ assert.False(t, m.HasValue())
})
t.Run("should panic if trying to access a value with MustGet", func(t *testing.T) {
defer func() {
err := recover()
- testutil.IsNotNil(t, err)
- testutil.Equals(t, "trying to access a monad's value but none is set", err.(string))
+ assert.NotNil(t, err)
+ assert.Equal(t, "trying to access a monad's value but none is set", err.(string))
}()
var m monad.Maybe[time.Time]
@@ -80,7 +80,7 @@ func Test_Maybe(t *testing.T) {
m := monad.Value(now)
- testutil.Equals(t, now, m.MustGet())
+ assert.Equal(t, now, m.MustGet())
})
t.Run("could returns its value or fallback if not set", func(t *testing.T) {
@@ -89,8 +89,8 @@ func Test_Maybe(t *testing.T) {
wValue = monad.Value("got a value")
)
- testutil.Equals(t, "got a value", wValue.Get("default"))
- testutil.Equals(t, "default", woValue.Get("default"))
+ assert.Equal(t, "got a value", wValue.Get("default"))
+ assert.Equal(t, "default", woValue.Get("default"))
})
t.Run("should implements the valuer interface", func(t *testing.T) {
@@ -98,15 +98,15 @@ func Test_Maybe(t *testing.T) {
driverValue, err := m.Value()
- testutil.IsNil(t, err)
- testutil.IsNil(t, driverValue)
+ assert.Nil(t, err)
+ assert.Nil(t, driverValue)
now := time.Now().UTC()
m.Set(now)
driverValue, err = m.Value()
- testutil.IsNil(t, err)
- testutil.IsTrue(t, driverValue == now)
+ assert.Nil(t, err)
+ assert.True(t, driverValue == now)
})
t.Run("should implements the Scanner interface", func(t *testing.T) {
@@ -114,14 +114,14 @@ func Test_Maybe(t *testing.T) {
err := m.Scan(nil)
- testutil.IsNil(t, err)
- testutil.IsFalse(t, m.HasValue())
+ assert.Nil(t, err)
+ assert.False(t, m.HasValue())
err = m.Scan("data")
- testutil.IsNil(t, err)
- testutil.IsTrue(t, m.HasValue())
- testutil.Equals(t, "data", m.MustGet())
+ assert.Nil(t, err)
+ assert.True(t, m.HasValue())
+ assert.Equal(t, "data", m.MustGet())
})
t.Run("should correctly marshal to json", func(t *testing.T) {
@@ -129,15 +129,15 @@ func Test_Maybe(t *testing.T) {
data, err := m.MarshalJSON()
- testutil.IsNil(t, err)
- testutil.Equals(t, "null", string(data))
+ assert.Nil(t, err)
+ assert.Equal(t, "null", string(data))
m.Set("ok")
data, err = m.MarshalJSON()
- testutil.IsNil(t, err)
- testutil.Equals(t, `"ok"`, string(data))
+ assert.Nil(t, err)
+ assert.Equal(t, `"ok"`, string(data))
})
t.Run("should correctly marshal to yaml", func(t *testing.T) {
@@ -145,17 +145,17 @@ func Test_Maybe(t *testing.T) {
data, err := m.MarshalYAML()
- testutil.IsNil(t, err)
- testutil.IsTrue(t, m.IsZero())
- testutil.IsNil(t, data)
+ assert.Nil(t, err)
+ assert.True(t, m.IsZero())
+ assert.Nil(t, data)
m.Set("ok")
data, err = m.MarshalYAML()
- testutil.IsNil(t, err)
- testutil.IsFalse(t, m.IsZero())
- testutil.Equals(t, "ok", data)
+ assert.Nil(t, err)
+ assert.False(t, m.IsZero())
+ assert.Equal(t, "ok", data)
})
t.Run("should correctly unmarshal from yaml", func(t *testing.T) {
@@ -163,9 +163,9 @@ func Test_Maybe(t *testing.T) {
err := m.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "ok"})
- testutil.IsNil(t, err)
- testutil.IsTrue(t, m.HasValue())
- testutil.Equals(t, "ok", m.MustGet())
+ assert.Nil(t, err)
+ assert.True(t, m.HasValue())
+ assert.Equal(t, "ok", m.MustGet())
})
t.Run("should correctly unmarshal from env variables", func(t *testing.T) {
@@ -173,12 +173,12 @@ func Test_Maybe(t *testing.T) {
err := m.UnmarshalEnvironmentValue("")
- testutil.IsNil(t, err)
- testutil.IsFalse(t, m.HasValue())
+ assert.Nil(t, err)
+ assert.False(t, m.HasValue())
err = m.UnmarshalEnvironmentValue("ok")
- testutil.IsNil(t, err)
- testutil.IsTrue(t, m.HasValue())
- testutil.Equals(t, "ok", m.MustGet())
+ assert.Nil(t, err)
+ assert.True(t, m.HasValue())
+ assert.Equal(t, "ok", m.MustGet())
})
}
diff --git a/pkg/monad/patch_test.go b/pkg/monad/patch_test.go
index 9773c8a2..1bfba74e 100644
--- a/pkg/monad/patch_test.go
+++ b/pkg/monad/patch_test.go
@@ -4,34 +4,34 @@ import (
"encoding/json"
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/monad"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Patch(t *testing.T) {
t.Run("should default to a not set, empty value", func(t *testing.T) {
var p monad.Patch[int]
- testutil.IsFalse(t, p.IsSet())
- testutil.IsFalse(t, p.IsNil())
- testutil.IsFalse(t, p.HasValue())
+ assert.False(t, p.IsSet())
+ assert.False(t, p.IsNil())
+ assert.False(t, p.HasValue())
})
t.Run("should be instantiable with a value", func(t *testing.T) {
p := monad.PatchValue(42)
- testutil.IsTrue(t, p.IsSet())
- testutil.IsFalse(t, p.IsNil())
- testutil.IsTrue(t, p.HasValue())
- testutil.Equals(t, 42, p.MustGet())
+ assert.True(t, p.IsSet())
+ assert.False(t, p.IsNil())
+ assert.True(t, p.HasValue())
+ assert.Equal(t, 42, p.MustGet())
})
t.Run("should be instantiable with a nil value", func(t *testing.T) {
p := monad.Nil[int]()
- testutil.IsTrue(t, p.IsSet())
- testutil.IsTrue(t, p.IsNil())
- testutil.IsFalse(t, p.HasValue())
+ assert.True(t, p.IsSet())
+ assert.True(t, p.IsNil())
+ assert.False(t, p.HasValue())
})
t.Run("should return the inner monad and a boolean indicating if it has been set", func(t *testing.T) {
@@ -50,8 +50,8 @@ func Test_Patch(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
m, isSet := test.value.TryGet()
- testutil.Equals(t, test.isSet, isSet)
- testutil.Equals(t, test.hasValue, m.HasValue())
+ assert.Equal(t, test.isSet, isSet)
+ assert.Equal(t, test.hasValue, m.HasValue())
})
}
})
@@ -72,10 +72,10 @@ func Test_Patch(t *testing.T) {
t.Run(test.json, func(t *testing.T) {
var value someStruct
- testutil.IsNil(t, json.Unmarshal([]byte(test.json), &value))
- testutil.Equals(t, test.isSet, value.Number.IsSet())
- testutil.Equals(t, test.isNil, value.Number.IsNil())
- testutil.Equals(t, test.hasValue, value.Number.HasValue())
+ assert.Nil(t, json.Unmarshal([]byte(test.json), &value))
+ assert.Equal(t, test.isSet, value.Number.IsSet())
+ assert.Equal(t, test.isNil, value.Number.IsNil())
+ assert.Equal(t, test.hasValue, value.Number.HasValue())
})
}
})
diff --git a/pkg/must/panic_test.go b/pkg/must/panic_test.go
index 0ff1ed01..a653427f 100644
--- a/pkg/must/panic_test.go
+++ b/pkg/must/panic_test.go
@@ -4,8 +4,8 @@ import (
"errors"
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/must"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Panic(t *testing.T) {
@@ -14,8 +14,8 @@ func Test_Panic(t *testing.T) {
defer func() {
r := recover()
- testutil.IsNotNil(t, r)
- testutil.ErrorIs(t, err, r.(error))
+ assert.NotNil(t, r)
+ assert.ErrorIs(t, err, r.(error))
}()
must.Panic(42, err)
@@ -24,6 +24,6 @@ func Test_Panic(t *testing.T) {
t.Run("should return the value if no error is given", func(t *testing.T) {
value := must.Panic(42, nil)
- testutil.Equals(t, 42, value)
+ assert.Equal(t, 42, value)
})
}
diff --git a/pkg/ostools/file.go b/pkg/ostools/file.go
index 48d39be7..a275ed59 100644
--- a/pkg/ostools/file.go
+++ b/pkg/ostools/file.go
@@ -1,6 +1,7 @@
package ostools
import (
+ "errors"
"io/fs"
"os"
"path/filepath"
@@ -8,6 +9,8 @@ import (
const defaultPermissions fs.FileMode = 0744
+var ErrTooManyPermissionsGiven = errors.New("too_many_permissions_given")
+
// Open or create the file to append data only. It also creates intermediate directories as needed.
func OpenAppend(name string) (*os.File, error) {
if err := MkdirAll(filepath.Dir(name)); err != nil {
@@ -25,10 +28,15 @@ func WriteFile(name string, data []byte, perm ...fs.FileMode) error {
return err
}
- filePermissions := defaultPermissions
+ var filePermissions fs.FileMode
- if len(perm) > 0 {
+ switch len(perm) {
+ case 0:
+ filePermissions = defaultPermissions
+ case 1:
filePermissions = perm[0]
+ default:
+ return ErrTooManyPermissionsGiven
}
return os.WriteFile(name, data, filePermissions)
diff --git a/pkg/ssh/config_test.go b/pkg/ssh/config_test.go
index 20c4fd01..54db22a0 100644
--- a/pkg/ssh/config_test.go
+++ b/pkg/ssh/config_test.go
@@ -6,11 +6,11 @@ import (
"path/filepath"
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/id"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/ostools"
"github.com/YuukanOO/seelf/pkg/ssh"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_FileConfigurator(t *testing.T) {
@@ -22,7 +22,7 @@ func Test_FileConfigurator(t *testing.T) {
})
if initialConfigContent != "" {
- ostools.WriteFile(path, []byte(initialConfigContent))
+ _ = ostools.WriteFile(path, []byte(initialConfigContent))
}
return ssh.NewFileConfigurator(path), path
@@ -31,29 +31,29 @@ func Test_FileConfigurator(t *testing.T) {
t.Run("should be able to create a new ssh config if none is found and append the host", func(t *testing.T) {
configurator, path := sut("")
- testutil.IsNil(t, configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Host: "example.com",
}))
- testutil.FileEquals(t, path, `Host example.com
+ assert.FileContentEquals(t, `Host example.com
StrictHostKeyChecking accept-new
-`)
+`, path)
})
t.Run("should correctly append a host to an existing config file", func(t *testing.T) {
configurator, path := sut("Host example.com\nUser root\n")
- testutil.IsNil(t, configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Host: "somewhere.com",
User: monad.Value("user"),
Port: monad.Value(2222),
}))
- testutil.FileEquals(t, path, `Host example.com
+ assert.FileContentEquals(t, `Host example.com
User root
Host somewhere.com
StrictHostKeyChecking accept-new
User user
Port 2222
-`)
+`, path)
})
t.Run("should correctly update an existing host", func(t *testing.T) {
@@ -65,18 +65,18 @@ User user
Port 2222
`)
- testutil.IsNil(t, configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Host: "somewhere.com",
User: monad.Value("root"),
Port: monad.Value(22),
}))
- testutil.FileEquals(t, path, `Host example.com
+ assert.FileContentEquals(t, `Host example.com
User root
Host somewhere.com
StrictHostKeyChecking accept-new
User root
Port 22
-`)
+`, path)
})
t.Run("should update an host only if the identifier match", func(t *testing.T) {
@@ -86,45 +86,45 @@ Host example.com #my-identifier
User john
`)
- testutil.IsNil(t, configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Identifier: "my-identifier",
Host: "another.com",
User: monad.Value("john"),
Port: monad.Value(2222),
}))
- testutil.FileEquals(t, path, `Host example.com
+ assert.FileContentEquals(t, `Host example.com
User root
Host another.com #my-identifier
StrictHostKeyChecking accept-new
User john
Port 2222
-`)
+`, path)
})
t.Run("should write the private key if set", func(t *testing.T) {
configurator, path := sut("")
expectedKeyPath := filepath.Join(filepath.Dir(path), "privkeyfilename")
- testutil.IsNil(t, configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Host: "example.com",
PrivateKey: monad.Value(ssh.ConnectionKey{
Name: "privkeyfilename",
Key: "privkeycontent",
}),
}))
- testutil.FileEquals(t, path, fmt.Sprintf(`Host example.com
+ assert.FileContentEquals(t, fmt.Sprintf(`Host example.com
StrictHostKeyChecking accept-new
IdentityFile %s
IdentitiesOnly yes
-`, expectedKeyPath))
- testutil.FileEquals(t, expectedKeyPath, "privkeycontent")
+`, expectedKeyPath), path)
+ assert.FileContentEquals(t, "privkeycontent", expectedKeyPath)
})
t.Run("should remove the old private key if it was set", func(t *testing.T) {
configurator, path := sut("")
oldKeyPath := filepath.Join(filepath.Dir(path), "oldkeyfilename")
newKeyPath := filepath.Join(filepath.Dir(path), "newkeyfilename")
- testutil.IsNil(t, configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Host: "example.com",
PrivateKey: monad.Value(ssh.ConnectionKey{
Name: "oldkeyfilename",
@@ -132,26 +132,26 @@ IdentitiesOnly yes
}),
}))
- testutil.IsNil(t, configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Host: "example.com",
PrivateKey: monad.Value(ssh.ConnectionKey{
Name: "newkeyfilename",
Key: "newprivkeycontent",
}),
}))
- testutil.FileEquals(t, path, fmt.Sprintf(`Host example.com
+ assert.FileContentEquals(t, fmt.Sprintf(`Host example.com
StrictHostKeyChecking accept-new
IdentityFile %s
IdentitiesOnly yes
-`, newKeyPath))
- testutil.FileEquals(t, newKeyPath, "newprivkeycontent")
- testutil.FileEquals(t, oldKeyPath, "")
+`, newKeyPath), path)
+ assert.FileContentEquals(t, "newprivkeycontent", newKeyPath)
+ assert.FileContentEquals(t, "", oldKeyPath)
})
t.Run("should do nothing if trying to delete an host and no config file exist", func(t *testing.T) {
configurator, _ := sut("")
- testutil.IsNil(t, configurator.Remove("test"))
+ assert.Nil(t, configurator.Remove("test"))
})
t.Run("should correctly remove an host", func(t *testing.T) {
@@ -161,10 +161,10 @@ Host example.com #my-identifier
User john
`)
- testutil.IsNil(t, configurator.Remove(""))
- testutil.FileEquals(t, path, `Host example.com #my-identifier
+ assert.Nil(t, configurator.Remove(""))
+ assert.FileContentEquals(t, `Host example.com #my-identifier
User john
-`)
+`, path)
})
t.Run("should correctly remove an host with a specific identifier", func(t *testing.T) {
@@ -174,25 +174,25 @@ Host example.com #my-identifier
User john
`)
- testutil.IsNil(t, configurator.Remove("my-identifier"))
- testutil.FileEquals(t, path, `Host example.com
+ assert.Nil(t, configurator.Remove("my-identifier"))
+ assert.FileContentEquals(t, `Host example.com
User root
-`)
+`, path)
})
t.Run("should remove the private key attached to the host being removed", func(t *testing.T) {
configurator, path := sut("")
keyPath := filepath.Join(filepath.Dir(path), "privkeyfilename")
- configurator.Upsert(ssh.Connection{
+ assert.Nil(t, configurator.Upsert(ssh.Connection{
Host: "example.com",
PrivateKey: monad.Value(ssh.ConnectionKey{
Name: "privkeyfilename",
Key: "privkeycontent",
}),
- })
+ }))
- testutil.IsNil(t, configurator.Remove(""))
- testutil.FileEquals(t, path, "")
- testutil.FileEquals(t, keyPath, "")
+ assert.Nil(t, configurator.Remove(""))
+ assert.FileContentEquals(t, "", path)
+ assert.FileContentEquals(t, "", keyPath)
})
}
diff --git a/pkg/ssh/host_test.go b/pkg/ssh/host_test.go
index 0a3b77f3..b15f9054 100644
--- a/pkg/ssh/host_test.go
+++ b/pkg/ssh/host_test.go
@@ -3,9 +3,9 @@ package ssh_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/must"
"github.com/YuukanOO/seelf/pkg/ssh"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Host(t *testing.T) {
@@ -30,18 +30,18 @@ func Test_Host(t *testing.T) {
got, err := ssh.ParseHost(tt.value)
if !tt.valid {
- testutil.ErrorIs(t, ssh.ErrInvalidHost, err)
+ assert.ErrorIs(t, ssh.ErrInvalidHost, err)
return
}
- testutil.IsNil(t, err)
- testutil.Equals(t, tt.value, string(got))
+ assert.Nil(t, err)
+ assert.Equal(t, tt.value, string(got))
})
}
})
t.Run("should returns a string representation", func(t *testing.T) {
h := must.Panic(ssh.ParseHost("localhost"))
- testutil.Equals(t, "localhost", h.String())
+ assert.Equal(t, "localhost", h.String())
})
}
diff --git a/pkg/ssh/private_key_test.go b/pkg/ssh/private_key_test.go
index ca1cac8f..e51444f4 100644
--- a/pkg/ssh/private_key_test.go
+++ b/pkg/ssh/private_key_test.go
@@ -3,8 +3,8 @@ package ssh_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/ssh"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_PrivateKey(t *testing.T) {
@@ -38,12 +38,12 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID
got, err := ssh.ParsePrivateKey(tt.value)
if !tt.valid {
- testutil.ErrorIs(t, ssh.ErrInvalidSSHKey, err)
+ assert.ErrorIs(t, ssh.ErrInvalidSSHKey, err)
return
}
- testutil.IsNil(t, err)
- testutil.Equals(t, tt.value, string(got))
+ assert.Nil(t, err)
+ assert.Equal(t, tt.value, string(got))
})
}
})
diff --git a/pkg/storage/discriminated.go b/pkg/storage/discriminated.go
index e782f1cb..8c87197b 100644
--- a/pkg/storage/discriminated.go
+++ b/pkg/storage/discriminated.go
@@ -29,6 +29,8 @@ func NewDiscriminatedMapper[T any](
}
// Register a new concrete type available to the mapper.
+// It will panic if a type is already registered with the same discriminator since it
+// should never happen.
func (m *DiscriminatedMapper[T]) Register(concreteType T, mapper DiscriminatedMapperFunc[T]) {
discriminator := m.extractor(concreteType)
diff --git a/pkg/storage/discriminated_test.go b/pkg/storage/discriminated_test.go
index e74552f8..dc9dc321 100644
--- a/pkg/storage/discriminated_test.go
+++ b/pkg/storage/discriminated_test.go
@@ -3,8 +3,8 @@ package storage_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/storage"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
type (
@@ -45,18 +45,18 @@ func Test_Discriminated(t *testing.T) {
t.Run("should error if the discriminator is not known", func(t *testing.T) {
_, err := mapper.From("unknown", "")
- testutil.ErrorIs(t, err, storage.ErrCouldNotUnmarshalGivenType)
+ assert.ErrorIs(t, err, storage.ErrCouldNotUnmarshalGivenType)
})
t.Run("should return the correct type", func(t *testing.T) {
t1, err := mapper.From("type1", "data1")
- testutil.IsNil(t, err)
- testutil.Equals(t, type1{"data1"}, t1.(type1))
+ assert.Nil(t, err)
+ assert.Equal(t, type1{"data1"}, t1.(type1))
t2, err := mapper.From("type2", "data2")
- testutil.IsNil(t, err)
- testutil.Equals(t, type2{"data2"}, t2.(type2))
+ assert.Nil(t, err)
+ assert.Equal(t, type2{"data2"}, t2.(type2))
})
}
diff --git a/pkg/storage/secret_string_test.go b/pkg/storage/secret_string_test.go
index e5271e59..a899825f 100644
--- a/pkg/storage/secret_string_test.go
+++ b/pkg/storage/secret_string_test.go
@@ -3,8 +3,8 @@ package storage_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/storage"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_SecretString(t *testing.T) {
@@ -13,8 +13,8 @@ func Test_SecretString(t *testing.T) {
err := s.Scan("test")
- testutil.IsNil(t, err)
- testutil.Equals(t, "test", s)
+ assert.Nil(t, err)
+ assert.Equal(t, "test", s)
})
t.Run("should marshal to a json string with the same length as the original string and custom characters", func(t *testing.T) {
@@ -23,8 +23,8 @@ func Test_SecretString(t *testing.T) {
data, err := s.MarshalJSON()
dataStr := string(data)
- testutil.IsNil(t, err)
- testutil.Equals(t, `"******************"`, dataStr)
+ assert.Nil(t, err)
+ assert.Equal(t, `"******************"`, dataStr)
})
t.Run("should keep newlines", func(t *testing.T) {
@@ -35,7 +35,7 @@ and another one`)
data, err := s.MarshalJSON()
dataStr := string(data)
- testutil.IsNil(t, err)
- testutil.Equals(t, `"******************\n**************\n***************"`, dataStr)
+ assert.Nil(t, err)
+ assert.Equal(t, `"******************\n**************\n***************"`, dataStr)
})
}
diff --git a/pkg/storage/sqlite/builder/builder_test.go b/pkg/storage/sqlite/builder/builder_test.go
index bcf59483..b8f424cb 100644
--- a/pkg/storage/sqlite/builder/builder_test.go
+++ b/pkg/storage/sqlite/builder/builder_test.go
@@ -3,9 +3,9 @@ package builder_test
import (
"testing"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/storage/sqlite/builder"
- "github.com/YuukanOO/seelf/pkg/testutil"
)
func Test_Builder(t *testing.T) {
@@ -13,7 +13,7 @@ func Test_Builder(t *testing.T) {
q := builder.
Query[any]("SELECT id, name FROM some_table WHERE name = ?", "john")
- testutil.Equals(t, "SELECT id, name FROM some_table WHERE name = ?", q.String())
+ assert.Equal(t, "SELECT id, name FROM some_table WHERE name = ?", q.String())
})
t.Run("should handle statements", func(t *testing.T) {
@@ -34,7 +34,7 @@ func Test_Builder(t *testing.T) {
).
F("ORDER BY name")
- testutil.Equals(t, "SELECT id, name FROM some_table WHERE name = ? AND id = ? AND age IN (?,?) AND TRUE ORDER BY name", q.String())
+ assert.Equal(t, "SELECT id, name FROM some_table WHERE name = ? AND id = ? AND age IN (?,?) AND TRUE ORDER BY name", q.String())
})
t.Run("should handle insert statements", func(t *testing.T) {
@@ -44,7 +44,7 @@ func Test_Builder(t *testing.T) {
"id": 1,
})
- testutil.Match(t, "INSERT INTO some_table \\((,?(age|name|id)){3}\\) VALUES \\(\\?,\\?,\\?\\)", q.String())
+ assert.Match(t, "INSERT INTO some_table \\((,?(age|name|id)){3}\\) VALUES \\(\\?,\\?,\\?\\)", q.String())
})
t.Run("should handle update statements", func(t *testing.T) {
@@ -53,6 +53,6 @@ func Test_Builder(t *testing.T) {
"age": 21,
}).F("WHERE id = ?", 1)
- testutil.Match(t, "UPDATE some_table SET (,?(age|name) = \\?){2} WHERE id = \\?", q.String())
+ assert.Match(t, "UPDATE some_table SET (,?(age|name) = \\?){2} WHERE id = \\?", q.String())
})
}
diff --git a/pkg/testutil/assertion.go b/pkg/testutil/assertion.go
deleted file mode 100644
index 5b3ce920..00000000
--- a/pkg/testutil/assertion.go
+++ /dev/null
@@ -1,130 +0,0 @@
-// Package testutil exposes assert utilities used in the project to make things
-// simpler to read.
-package testutil
-
-import (
- "errors"
- "os"
- "reflect"
- "regexp"
- "strings"
- "testing"
- "unicode/utf8"
-
- "github.com/YuukanOO/seelf/pkg/event"
-)
-
-func Equals[T comparable](t testing.TB, expected, actual T) {
- if expected != actual {
- expectationVersusReality(t, "should have been equals", expected, actual)
- }
-}
-
-func NotEquals[T comparable](t testing.TB, expected, actual T) {
- if expected == actual {
- expectationVersusReality(t, "should not have been equals", expected, actual)
- }
-}
-
-func DeepEquals[T any](t testing.TB, expected, actual T) {
- if !reflect.DeepEqual(expected, actual) {
- expectationVersusReality(t, "should have been deeply equals", expected, actual)
- }
-}
-
-func IsTrue[T ~bool](t testing.TB, expr T) {
- Equals(t, true, expr)
-}
-
-func IsFalse[T ~bool](t testing.TB, expr T) {
- Equals(t, false, expr)
-}
-
-func IsNil(t testing.TB, expr any) {
- if expr != nil {
- expectationVersusReality(t, "should have been nil", nil, expr)
- }
-}
-
-func IsNotNil(t testing.TB, expr any) {
- if expr == nil {
- expectationVersusReality(t, "should have been not nil", "nothing but ", expr)
- }
-}
-
-func HasLength[T any](t testing.TB, arr []T, length int) {
- actual := len(arr)
- if actual != length {
- expectationVersusReality(t, "should have correct size", length, actual)
- }
-}
-
-func HasNChars[T ~string](t testing.TB, expected int, value T) {
- actual := utf8.RuneCountInString(string(value))
-
- if actual != expected {
- expectationVersusReality(t, "should have correct number of characters", expected, actual)
- }
-}
-
-func Contains(t testing.TB, expected string, value string) {
- if !strings.Contains(value, expected) {
- expectationVersusReality(t, "should contains the string", expected, value)
- }
-}
-
-func Match(t testing.TB, re string, value string) {
- if !regexp.MustCompile(re).MatchString(value) {
- expectationVersusReality(t, "should match", re, value)
- }
-}
-
-func ErrorIs(t testing.TB, expected, actual error) {
- if !errors.Is(actual, expected) {
- expectationVersusReality(t, "errors should have match", expected, actual)
- }
-}
-
-func HasNEvents(t testing.TB, source event.Source, expected int) {
- actual := len(event.Unwrap(source))
-
- if actual != expected {
- expectationVersusReality(t, "should have correct number of events", expected, actual)
- }
-}
-
-func EventIs[T event.Event](t testing.TB, source event.Source, index int) (result T) {
- events := event.Unwrap(source)
-
- if index >= len(events) {
- expectationVersusReality(t, "could not find an event at given index", index, nil)
- return result
- }
-
- result, ok := events[index].(T)
-
- if !ok {
- expectationVersusReality(t, "wrong event type", events[index], result)
- return result
- }
-
- return result
-}
-
-func FileEquals(t testing.TB, path, expected string) {
- data, _ := os.ReadFile(path)
- str := string(data)
-
- if str != expected {
- expectationVersusReality(t, "file content should have been equals", expected, str)
- }
-}
-
-func expectationVersusReality(t testing.TB, message string, expected, actual any) {
- t.Fatalf(`%s
- expected:
-%v
-
- got:
-%v`, message, expected, actual)
-}
diff --git a/pkg/testutil/assertion_test.go b/pkg/testutil/assertion_test.go
deleted file mode 100644
index fbddedaa..00000000
--- a/pkg/testutil/assertion_test.go
+++ /dev/null
@@ -1,432 +0,0 @@
-package testutil_test
-
-import (
- "errors"
- "fmt"
- "os"
- "testing"
-
- "github.com/YuukanOO/seelf/pkg/bus"
- "github.com/YuukanOO/seelf/pkg/event"
- "github.com/YuukanOO/seelf/pkg/testutil"
-)
-
-type testMock struct {
- testing.TB
- hasFailed bool
-}
-
-func (t *testMock) Fatalf(format string, args ...any) {
- // TODO: must test the error message too
- t.hasFailed = true
-}
-
-func Test_Equals(t *testing.T) {
- tests := []struct {
- expected bool
- actual bool
- shouldFail bool
- }{
- {true, false, true},
- {true, true, false},
- {false, false, false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.Equals(mock, test.expected, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_NotEquals(t *testing.T) {
- tests := []struct {
- expected bool
- actual bool
- shouldFail bool
- }{
- {true, true, true},
- {false, true, false},
- {false, false, true},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.NotEquals(mock, test.expected, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_DeepEquals(t *testing.T) {
- tests := []struct {
- expected []bool
- actual []bool
- shouldFail bool
- }{
- {[]bool{true, true}, []bool{false, true}, true},
- {[]bool{true, true}, []bool{true, true}, false},
- {[]bool{false, false}, []bool{false, true}, true},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.DeepEquals(mock, test.expected, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_IsTrue(t *testing.T) {
- tests := []struct {
- actual bool
- shouldFail bool
- }{
- {true, false},
- {false, true},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.IsTrue(mock, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_IsFalse(t *testing.T) {
- tests := []struct {
- actual bool
- shouldFail bool
- }{
- {true, true},
- {false, false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.IsFalse(mock, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_IsNil(t *testing.T) {
- tests := []struct {
- actual any
- shouldFail bool
- }{
- {true, true},
- {nil, false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.IsNil(mock, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_IsNotNil(t *testing.T) {
- tests := []struct {
- actual any
- shouldFail bool
- }{
- {true, false},
- {nil, true},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.IsNotNil(mock, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_HasLength(t *testing.T) {
- tests := []struct {
- expected int
- actual []int
- shouldFail bool
- }{
- {1, []int{1, 2}, true},
- {2, []int{1, 2}, false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.HasLength(mock, test.actual, test.expected)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_HasNChars(t *testing.T) {
- tests := []struct {
- expected int
- actual string
- shouldFail bool
- }{
- {5, "a long string", true},
- {2, "hi", false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.HasNChars(mock, test.expected, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_Contains(t *testing.T) {
- tests := []struct {
- value string
- search string
- shouldFail bool
- }{
- {"validation failed", "error", true},
- {"validation failed", "failed", false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.value, test.search), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.Contains(mock, test.search, test.value)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_Match(t *testing.T) {
- tests := []struct {
- re string
- value string
- shouldFail bool
- }{
- {"abc", "error", true},
- {"abc", "abc", false},
- {"abc?", "ab", false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.value, test.re), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.Match(mock, test.re, test.value)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_ErrorIs(t *testing.T) {
- err := errors.New("some error")
-
- tests := []struct {
- expected error
- actual error
- shouldFail bool
- }{
- {err, errors.New("another one"), true},
- {err, err, false},
- {err, fmt.Errorf("with wrapped error %w", err), false},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.ErrorIs(mock, test.expected, test.actual)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-type (
- domainEntity struct {
- event.Emitter
- }
-
- eventA struct {
- bus.Notification
- msg string
- }
-
- eventB struct {
- bus.Notification
- number int
- }
-)
-
-func (eventA) Name_() string { return "eventA" }
-func (eventB) Name_() string { return "eventB" }
-
-func Test_EventIs(t *testing.T) {
- var entity domainEntity
-
- entity = entity.apply(eventA{msg: "test"}).apply(eventB{number: 42})
-
- t.Run("should be able to retrieve an event if it exists", func(t *testing.T) {
- evt := testutil.EventIs[eventA](t, &entity, 0)
- evt2 := testutil.EventIs[eventB](t, &entity, 1)
-
- testutil.Equals(t, "test", evt.msg)
- testutil.Equals(t, 42, evt2.number)
- })
-
- t.Run("should fail if no events exists at all", func(t *testing.T) {
- mock := new(testMock)
-
- testutil.EventIs[eventA](mock, &domainEntity{}, 0)
-
- if !mock.hasFailed {
- t.Fail()
- }
- })
-
- t.Run("should fail if trying to access a not in range index", func(t *testing.T) {
- mock := new(testMock)
-
- testutil.EventIs[eventA](mock, &entity, 2)
-
- if !mock.hasFailed {
- t.Fail()
- }
- })
-
- t.Run("should fail if type does not match", func(t *testing.T) {
- mock := new(testMock)
- testutil.EventIs[eventB](mock, &entity, 0)
-
- if !mock.hasFailed {
- t.Fail()
- }
- })
-}
-
-func Test_HasNEvents(t *testing.T) {
- var entity domainEntity
-
- entity = entity.apply(eventA{msg: "test"}).apply(eventB{number: 42})
-
- tests := []struct {
- expected int
- shouldFail bool
- }{
- {1, true},
- {2, false},
- {4, true},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v", test.expected), func(t *testing.T) {
- mock := new(testMock)
-
- testutil.HasNEvents(mock, &entity, test.expected)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
- }
-}
-
-func Test_FileEquals(t *testing.T) {
- path := "testfile"
-
- t.Cleanup(func() {
- os.RemoveAll(path)
- })
-
- tests := []struct {
- actual string
- expected string
- shouldFail bool
- }{
- {"test", "test", false},
- {"test", "test2", true},
- {"", "test", true},
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("%v %v", test.actual, test.expected), func(t *testing.T) {
- if test.actual != "" {
- os.WriteFile(path, []byte(test.actual), 0644)
- } else {
- os.RemoveAll(path)
- }
-
- mock := new(testMock)
-
- testutil.FileEquals(mock, path, test.expected)
-
- if mock.hasFailed != test.shouldFail {
- t.Fail()
- }
- })
-
- }
-}
-
-func (d domainEntity) apply(e event.Event) domainEntity {
- event.Store(&d, e)
- return d
-}
diff --git a/pkg/types/is_test.go b/pkg/types/is_test.go
index 620cf0c6..7dd76e99 100644
--- a/pkg/types/is_test.go
+++ b/pkg/types/is_test.go
@@ -3,7 +3,7 @@ package types_test
import (
"testing"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/types"
)
@@ -19,7 +19,7 @@ func Test_Is(t *testing.T) {
t2 any = type2{}
)
- testutil.IsTrue(t, types.Is[type1](t1))
- testutil.IsFalse(t, types.Is[type1](t2))
+ assert.True(t, types.Is[type1](t1))
+ assert.False(t, types.Is[type1](t2))
})
}
diff --git a/pkg/validate/numbers/numbers_test.go b/pkg/validate/numbers/numbers_test.go
index 5ee92865..2e9098f0 100644
--- a/pkg/validate/numbers/numbers_test.go
+++ b/pkg/validate/numbers/numbers_test.go
@@ -3,18 +3,18 @@ package numbers_test
import (
"testing"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/validate/numbers"
)
func Test_Min(t *testing.T) {
t.Run("should fail on value lesser than the required min", func(t *testing.T) {
- testutil.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(2))
- testutil.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(1))
+ assert.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(2))
+ assert.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(1))
})
t.Run("should succeed on value greater then the required min", func(t *testing.T) {
- testutil.IsNil(t, numbers.Min(3)(4))
- testutil.IsNil(t, numbers.Min(3)(3))
+ assert.Nil(t, numbers.Min(3)(4))
+ assert.Nil(t, numbers.Min(3)(3))
})
}
diff --git a/pkg/validate/strings/strings_test.go b/pkg/validate/strings/strings_test.go
index 380cad42..12cb6cb1 100644
--- a/pkg/validate/strings/strings_test.go
+++ b/pkg/validate/strings/strings_test.go
@@ -4,18 +4,18 @@ import (
"regexp"
"testing"
- "github.com/YuukanOO/seelf/pkg/testutil"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/validate/strings"
)
func Test_Required(t *testing.T) {
t.Run("should fail on empty or whitespaced strings", func(t *testing.T) {
- testutil.ErrorIs(t, strings.ErrRequired, strings.Required(""))
- testutil.ErrorIs(t, strings.ErrRequired, strings.Required(" "))
+ assert.ErrorIs(t, strings.ErrRequired, strings.Required(""))
+ assert.ErrorIs(t, strings.ErrRequired, strings.Required(" "))
})
t.Run("should succeed on non-empty strings", func(t *testing.T) {
- testutil.IsNil(t, strings.Required("should be good"))
+ assert.Nil(t, strings.Required("should be good"))
})
}
@@ -23,33 +23,33 @@ func Test_Match(t *testing.T) {
reUrlFormat := regexp.MustCompile("^https?://.+")
t.Run("should fail on non matching strings", func(t *testing.T) {
- testutil.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("some string"))
- testutil.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("http://"))
+ assert.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("some string"))
+ assert.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("http://"))
})
t.Run("should succeed when matching", func(t *testing.T) {
- testutil.IsNil(t, strings.Match(reUrlFormat)("http://docker.localhost"))
+ assert.Nil(t, strings.Match(reUrlFormat)("http://docker.localhost"))
})
}
func Test_Min(t *testing.T) {
t.Run("should fail on strings with less characters than the given length", func(t *testing.T) {
- testutil.ErrorIs(t, strings.ErrMinLength, strings.Min(5)(""))
- testutil.ErrorIs(t, strings.ErrMinLength, strings.Min(5)("test"))
+ assert.ErrorIs(t, strings.ErrMinLength, strings.Min(5)(""))
+ assert.ErrorIs(t, strings.ErrMinLength, strings.Min(5)("test"))
})
t.Run("should succeed when enough characters are given", func(t *testing.T) {
- testutil.IsNil(t, strings.Min(5)("should be good"))
+ assert.Nil(t, strings.Min(5)("should be good"))
})
}
func Test_Max(t *testing.T) {
t.Run("should fail on strings with more characters than the given length", func(t *testing.T) {
- testutil.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("should not be good"))
- testutil.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("errorr"))
+ assert.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("should not be good"))
+ assert.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("errorr"))
})
t.Run("should succeed when less characters than length are given", func(t *testing.T) {
- testutil.IsNil(t, strings.Max(5)("yeah!"))
+ assert.Nil(t, strings.Max(5)("yeah!"))
})
}
diff --git a/pkg/validate/validate_test.go b/pkg/validate/validate_test.go
index b49404a6..35afcc34 100644
--- a/pkg/validate/validate_test.go
+++ b/pkg/validate/validate_test.go
@@ -6,8 +6,8 @@ import (
"testing"
"github.com/YuukanOO/seelf/pkg/apperr"
+ "github.com/YuukanOO/seelf/pkg/assert"
"github.com/YuukanOO/seelf/pkg/monad"
- "github.com/YuukanOO/seelf/pkg/testutil"
"github.com/YuukanOO/seelf/pkg/validate"
)
@@ -30,17 +30,17 @@ func alwaysFail(value string) error {
func Test_Field(t *testing.T) {
t.Run("call every validators", func(t *testing.T) {
err := validate.Field("", required, alwaysFail)
- testutil.ErrorIs(t, errRequired, err)
+ assert.ErrorIs(t, errRequired, err)
})
t.Run("returns nil when validation pass successfully", func(t *testing.T) {
err := validate.Field("something", required)
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
})
t.Run("returns the validator error", func(t *testing.T) {
err := validate.Field("something", required, alwaysFail)
- testutil.ErrorIs(t, errAlwaysFail, err)
+ assert.ErrorIs(t, errAlwaysFail, err)
})
}
@@ -59,16 +59,16 @@ func Test_Value(t *testing.T) {
var target objectValue
err := validate.Value("", &target, objectValueFactory)
- testutil.ErrorIs(t, errRequired, err)
- testutil.Equals(t, "", target)
+ assert.ErrorIs(t, errRequired, err)
+ assert.Equal(t, "", target)
})
t.Run("returns nil error and assign the target upon success", func(t *testing.T) {
var target objectValue
err := validate.Value("something", &target, objectValueFactory)
- testutil.IsNil(t, err)
- testutil.Equals(t, "something", target)
+ assert.Nil(t, err)
+ assert.Equal(t, "something", target)
})
}
@@ -79,15 +79,13 @@ func Test_Struct(t *testing.T) {
"lastName": validate.Field("doe", required, alwaysFail),
})
- testutil.Contains(t, "validation_failed:", err.Error())
- testutil.Contains(t, "firstName: required", err.Error())
- testutil.Contains(t, "lastName: always fail", err.Error())
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
- validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.Equals(t, 2, len(validationErr))
- testutil.ErrorIs(t, errRequired, validationErr["firstName"])
- testutil.ErrorIs(t, errAlwaysFail, validationErr["lastName"])
+ assert.Match(t, "validation_failed:", err.Error())
+ assert.Match(t, "firstName: required", err.Error())
+ assert.Match(t, "lastName: always fail", err.Error())
+ assert.ValidationError(t, validate.FieldErrors{
+ "firstName": errRequired,
+ "lastName": errAlwaysFail,
+ }, err)
})
t.Run("merge nested validation errors", func(t *testing.T) {
@@ -103,12 +101,12 @@ func Test_Struct(t *testing.T) {
})
validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.Equals(t, 4, len(validationErr))
- testutil.ErrorIs(t, errRequired, validationErr["firstName"])
- testutil.ErrorIs(t, errAlwaysFail, validationErr["lastName"])
- testutil.ErrorIs(t, errRequired, validationErr["nested.firstName"])
- testutil.ErrorIs(t, errRequired, validationErr["nested.nested.firstName"])
+ assert.True(t, ok)
+ assert.Equal(t, 4, len(validationErr))
+ assert.ErrorIs(t, errRequired, validationErr["firstName"])
+ assert.ErrorIs(t, errAlwaysFail, validationErr["lastName"])
+ assert.ErrorIs(t, errRequired, validationErr["nested.firstName"])
+ assert.ErrorIs(t, errRequired, validationErr["nested.nested.firstName"])
})
t.Run("returns nil if no error exists", func(t *testing.T) {
@@ -117,7 +115,7 @@ func Test_Struct(t *testing.T) {
"lastName": validate.Field("doe", required),
})
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
})
}
@@ -128,12 +126,12 @@ func Test_If(t *testing.T) {
"lastName": validate.If(true, func() error { return validate.Field("", required) }),
})
- testutil.Equals(t, `validation_failed:
+ assert.Equal(t, `validation_failed:
lastName: required`, err.Error())
validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.Equals(t, 1, len(validationErr))
- testutil.ErrorIs(t, errRequired, validationErr["lastName"])
+ assert.True(t, ok)
+ assert.Equal(t, 1, len(validationErr))
+ assert.ErrorIs(t, errRequired, validationErr["lastName"])
})
}
@@ -145,7 +143,7 @@ func Test_Maybe(t *testing.T) {
return validate.Field(val, required)
})
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
})
t.Run("executes the function if the monad is set", func(t *testing.T) {
@@ -155,7 +153,7 @@ func Test_Maybe(t *testing.T) {
return validate.Field(val, required)
})
- testutil.ErrorIs(t, errRequired, err)
+ assert.ErrorIs(t, errRequired, err)
})
}
@@ -167,7 +165,7 @@ func Test_Patch(t *testing.T) {
return validate.Field(val, required)
})
- testutil.IsNil(t, err)
+ assert.Nil(t, err)
})
t.Run("executes the function if the patch is set", func(t *testing.T) {
@@ -177,7 +175,7 @@ func Test_Patch(t *testing.T) {
return validate.Field(val, required)
})
- testutil.ErrorIs(t, errRequired, err)
+ assert.ErrorIs(t, errRequired, err)
})
}
@@ -185,26 +183,26 @@ func Test_Wrap(t *testing.T) {
t.Run("returns the error if it's not an application level error", func(t *testing.T) {
infrastructureErr := errors.New("an infrastructure error")
- testutil.IsTrue(t, validate.Wrap(infrastructureErr, "one", "two") == infrastructureErr)
- testutil.IsTrue(t, validate.Wrap(nil, "one", "two") == nil)
+ assert.True(t, validate.Wrap(infrastructureErr, "one", "two") == infrastructureErr)
+ assert.True(t, validate.Wrap(nil, "one", "two") == nil)
})
t.Run("returns nil if no err is given", func(t *testing.T) {
- testutil.IsNil(t, validate.Wrap(nil, "one", "two"))
+ assert.Nil(t, validate.Wrap(nil, "one", "two"))
})
t.Run("wrap the application error for the specified fields", func(t *testing.T) {
appErr := apperr.New("application level error")
err := validate.Wrap(appErr, "one", "two")
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
+ assert.ErrorIs(t, validate.ErrValidationFailed, err)
validationErr, ok := apperr.As[validate.FieldErrors](err)
fmt.Println(validationErr.Error())
- testutil.IsTrue(t, ok)
- testutil.Equals(t, 2, len(validationErr))
- testutil.ErrorIs(t, appErr, validationErr["one"])
- testutil.ErrorIs(t, appErr, validationErr["two"])
+ assert.True(t, ok)
+ assert.Equal(t, 2, len(validationErr))
+ assert.ErrorIs(t, appErr, validationErr["one"])
+ assert.ErrorIs(t, appErr, validationErr["two"])
})
t.Run("flatten nested validation errors", func(t *testing.T) {
@@ -215,14 +213,14 @@ func Test_Wrap(t *testing.T) {
err := validate.Wrap(appErr, "one", "two")
- testutil.ErrorIs(t, validate.ErrValidationFailed, err)
+ assert.ErrorIs(t, validate.ErrValidationFailed, err)
validationErr, ok := apperr.As[validate.FieldErrors](err)
- testutil.IsTrue(t, ok)
- testutil.Equals(t, 4, len(validationErr))
- testutil.ErrorIs(t, errRequired, validationErr["one.firstName"])
- testutil.ErrorIs(t, errAlwaysFail, validationErr["one.lastName"])
- testutil.ErrorIs(t, errRequired, validationErr["two.firstName"])
- testutil.ErrorIs(t, errAlwaysFail, validationErr["two.lastName"])
+ assert.True(t, ok)
+ assert.Equal(t, 4, len(validationErr))
+ assert.ErrorIs(t, errRequired, validationErr["one.firstName"])
+ assert.ErrorIs(t, errAlwaysFail, validationErr["one.lastName"])
+ assert.ErrorIs(t, errRequired, validationErr["two.firstName"])
+ assert.ErrorIs(t, errAlwaysFail, validationErr["two.lastName"])
})
}
@@ -239,7 +237,7 @@ func Test_FieldErrors(t *testing.T) {
"4": nil,
}.Flatten()
- testutil.DeepEquals(t, validate.FieldErrors{
+ assert.DeepEqual(t, validate.FieldErrors{
"1": errRequired,
"2.1": errAlwaysFail,
"3.1": errRequired,