diff --git a/Dockerfile b/Dockerfile index 4ca2580d..e50fd5a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ COPY go.* ./ RUN go mod download COPY . . COPY --from=front_builder /app/build ./cmd/serve/front/build -RUN go build -ldflags="-s -w" -o seelf +RUN make build-back FROM alpine:3.16 LABEL org.opencontainers.image.authors="julien@leicher.me" \ diff --git a/Makefile b/Makefile index 47c61290..0d13b734 100644 --- a/Makefile +++ b/Makefile @@ -7,20 +7,28 @@ serve-docs: # Launch the docs dev server serve-back: # Launch the backend API and creates an admin user if needed ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=admin LOG_LEVEL=debug go run main.go serve -test: # Launch every tests +test-front: # Launch the frontend tests cd cmd/serve/front && npm i && npm test && cd ../../.. + +test-back: # Launch the backend tests go vet ./... go test ./... --cover +test: test-front test-back # Launch every tests + ts: # Print the current timestamp, useful for migrations @date +%s outdated: # Print direct dependencies and their latest version go list -v -u -m -f '{{if not .Indirect}}{{.}}{{end}}' all -build: # Build the final binary for the current platform +build-front: # Build the frontend cd cmd/serve/front && npm i && npm run build && cd ../../.. - go build -ldflags="-s -w" -o seelf + +build-back: # Build the backend + go build -tags release -ldflags="-s -w" -o seelf + +build: build-front build-back # Build the final binary for the current platform build-docs: # Build the docs npm i && npm run docs:build diff --git a/cmd/config/configuration.go b/cmd/config/configuration.go index a50b6bc9..a8b81019 100644 --- a/cmd/config/configuration.go +++ b/cmd/config/configuration.go @@ -16,6 +16,7 @@ import ( "github.com/YuukanOO/seelf/pkg/log" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/must" + "github.com/YuukanOO/seelf/pkg/ostools" "github.com/YuukanOO/seelf/pkg/validate" "github.com/YuukanOO/seelf/pkg/validate/numbers" ) @@ -139,6 +140,11 @@ func (c *configuration) Initialize(logger log.ConfigurableLogger, path string) e return err } + // Make sure the data path exists + if err = ostools.MkdirAll(c.Data.Path); err != nil { + return err + } + // Update logger based on loaded configuration if err = logger.Configure(c.logFormat, c.logLevel); err != nil { return err 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/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..e2954f48 --- /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 err := result.UsersStore.Write(result.Context, s.users...); err != nil { + t.Fatal(err) + } + + if len(s.users) > 0 { + 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/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/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..65115de2 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,42 @@ 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) + return unExpose(ctx, reader, writer, evt.ProductionConfig.Target(), evt.ID) } - productionTarget, err := reader.GetByID(ctx, evt.ProductionConfig.Target()) - - if err != nil { + if err := unExpose(ctx, reader, writer, evt.ProductionConfig.Target(), evt.ID); err != nil { return err } - productionTarget.UnExposeEntrypoints(evt.ID, domain.Production) - - stagingTarget, err := reader.GetByID(ctx, evt.StagingConfig.Target()) - - if err != nil { + if err := unExpose(ctx, reader, writer, evt.StagingConfig.Target(), evt.ID); err != nil { return err } - stagingTarget.UnExposeEntrypoints(evt.ID, domain.Staging) + return nil + } +} + +func unExpose( + ctx context.Context, + reader domain.TargetsReader, + writer domain.TargetsWriter, + id domain.TargetID, + app domain.AppID, +) error { + target, err := reader.GetByID(ctx, id) - return writer.Write(ctx, &productionTarget, &stagingTarget) + if err != nil { + return err } + + target.UnExposeEntrypoints(app) + + 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_test.go b/internal/deployment/app/create_target/create_target_test.go index 735c7365..20d2f29a 100644 --- a/internal/deployment/app/create_target/create_target_test.go +++ b/internal/deployment/app/create_target/create_target_test.go @@ -4,119 +4,111 @@ 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/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, + "url": domain.ErrInvalidUrl, + }, 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), + fixture.WithTargetUrl(must.Panic(domain.UrlFrom("http://example.com"))), + ) - 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", 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) - }) - - 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)) - - uc := sut(&target) - - _, err := uc(ctx, create_target.Command{ - Name: "target", - Url: "http://another.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.ErrConfigAlreadyTaken, validateError[config.Kind()]) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) 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", Provider: config, }) - testutil.IsNil(t, err) - testutil.NotEquals(t, "", id) + assert.Nil(t, err) + assert.NotZero(t, id) + assert.HasLength(t, 1, dispatcher.Signals()) + + created := assert.Is[domain.TargetCreated](t, dispatcher.Signals()[0]) + assert.DeepEqual(t, domain.TargetCreated{ + ID: domain.TargetID(id), + Name: "target", + Url: must.Panic(domain.UrlFrom("http://example.com")), + 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) }) } -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/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/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/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/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_test.go b/internal/deployment/app/update_target/update_target_test.go index 44a93780..1e2a0612 100644 --- a/internal/deployment/app/update_target/update_target_test.go +++ b/internal/deployment/app/update_target/update_target_test.go @@ -4,97 +4,117 @@ 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.WithTargetUrl(must.Panic(domain.UrlFrom("http://localhost"))), + fixture.WithProviderConfig(config), + ) + targetTwo := fixture.Target( + fixture.WithTargetCreatedBy(user.ID()), + fixture.WithTargetUrl(must.Panic(domain.UrlFrom("http://docker.localhost"))), + ) + 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.Value("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) - - id, err := uc(context.Background(), update_target.Command{ + user := authfixture.User() + target := fixture.Target( + fixture.WithTargetCreatedBy(user.ID()), + fixture.WithProviderConfig(fixture.ProviderConfig(fixture.WithFingerprint("test"))), + ) + handler, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + newConfig := fixture.ProviderConfig(fixture.WithFingerprint("test")) + + id, err := handler(context.Background(), update_target.Command{ ID: string(target.ID()), Name: monad.Value("new name"), - Provider: "1", + Provider: newConfig, Url: monad.Value("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/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..df9ec2e1 100644 --- a/internal/deployment/domain/deployment.go +++ b/internal/deployment/domain/deployment.go @@ -44,7 +44,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 +52,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 } diff --git a/internal/deployment/domain/deployment_config.go b/internal/deployment/domain/deployment_config.go index 2dee38ec..1cc435c0 100644 --- a/internal/deployment/domain/deployment_config.go +++ b/internal/deployment/domain/deployment_config.go @@ -7,7 +7,7 @@ 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 { appid AppID @@ -95,7 +95,7 @@ func (c DeploymentConfig) QualifiedName(service string) string { return c.ProjectName() + "-" + service } -// Retrieve the name of the project wich is the combination of the appname, environment and appid +// Retrieve the name of the project which 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/deployment_config_test.go b/internal/deployment/domain/deployment_config_test.go index b2caad78..0b2be6bd 100644 --- a/internal/deployment/domain/deployment_config_test.go +++ b/internal/deployment/domain/deployment_config_test.go @@ -6,85 +6,93 @@ 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_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) { + 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)) 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()) + assert.Nil(t, err) + 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 fail if env is not valid", func(t *testing.T) { + app := fixture.App() _, err := app.ConfigSnapshotFor("invalid") - testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, err) + assert.ErrorIs(t, domain.ErrInvalidEnvironmentName, err) }) 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)) conf, _ := app.ConfigSnapshotFor(domain.Production) - testutil.IsFalse(t, conf.EnvironmentVariablesFor("otherservice").HasValue()) - testutil.IsTrue(t, conf.EnvironmentVariablesFor("app").HasValue()) - testutil.DeepEquals(t, domain.EnvVars{ + 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() conf, _ := app.ConfigSnapshotFor(domain.Staging) - testutil.IsFalse(t, conf.EnvironmentVariablesFor("app").HasValue()) + assert.False(t, conf.EnvironmentVariablesFor("app").HasValue()) }) t.Run("should generate a subdomain equals to app name if env is production", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) conf, _ := app.ConfigSnapshotFor(domain.Production) - testutil.Equals(t, "my-app", conf.SubDomain("app", true)) - testutil.Equals(t, "db.my-app", conf.SubDomain("db", false)) + assert.Equal(t, "my-app", conf.SubDomain("app", true)) + assert.Equal(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) { + app := fixture.App(fixture.WithAppName("my-app")) 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)) + assert.Equal(t, "my-app-staging", conf.SubDomain("app", true)) + assert.Equal(t, "db.my-app-staging", conf.SubDomain("db", false)) }) t.Run("should expose a unique project name", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) conf, _ := app.ConfigSnapshotFor(domain.Staging) - testutil.Equals(t, fmt.Sprintf("my-app-staging-%s", appidLower), conf.ProjectName()) + assert.Equal(t, fmt.Sprintf("my-app-staging-%s", strings.ToLower(string(app.ID()))), conf.ProjectName()) }) t.Run("should expose a unique image name for a service", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) conf, _ := app.ConfigSnapshotFor(domain.Staging) - testutil.Equals(t, fmt.Sprintf("my-app-%s/app:staging", appidLower), conf.ImageName("app")) + assert.Equal(t, fmt.Sprintf("my-app-%s/app:staging", strings.ToLower(string(app.ID()))), conf.ImageName("app")) }) t.Run("should expose a unique qualified name for a service", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) conf, _ := app.ConfigSnapshotFor(domain.Staging) - testutil.Equals(t, fmt.Sprintf("my-app-staging-%s-app", appidLower), conf.QualifiedName("app")) + assert.Equal(t, fmt.Sprintf("my-app-staging-%s-app", strings.ToLower(string(app.ID()))), 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..b07b5cd6 100644 --- a/internal/deployment/domain/deployment_test.go +++ b/internal/deployment/domain/deployment_test.go @@ -6,124 +6,122 @@ 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 + dpl := fixture.Deployment() - dpl, err := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) + err := dpl.HasStarted() - testutil.IsNil(t, err) + assert.Nil(t, err) + assert.HasNEvents(t, 2, &dpl) + evt := assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 1) - err = dpl.HasStarted() - - testutil.IsNil(t, err) - testutil.HasNEvents(t, &dpl, 2) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 1) - - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, domain.DeploymentStatusRunning, evt.State.Status()) + assert.Equal(t, dpl.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusRunning, evt.State.Status()) + assert.False(t, evt.State.ErrCode().HasValue()) + assert.False(t, evt.State.Services().HasValue()) + assert.NotZero(t, evt.State.StartedAt()) }) t.Run("could be marked has ended with services", func(t *testing.T) { - var err error - - dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Production, uid)) + dpl := fixture.Deployment() services := domain.Services{ dpl.Config().NewService("aservice", "an/image"), } + assert.Nil(t, dpl.HasStarted()) - dpl.HasStarted() + err := dpl.HasEnded(services, nil) - err = dpl.HasEnded(services, nil) + assert.Nil(t, err) + assert.HasNEvents(t, 3, &dpl, "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, 1) - testutil.Equals(t, domain.DeploymentStatusRunning, evt.State.Status()) + evt := assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 1) + assert.Equal(t, dpl.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusRunning, evt.State.Status()) - evt = testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) + evt = assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status()) - testutil.DeepEquals(t, services, evt.State.Services().MustGet()) + assert.Equal(t, dpl.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusSucceeded, evt.State.Status()) + assert.DeepEqual(t, services, evt.State.Services().MustGet()) }) t.Run("should default to a deployment without services if has ended without services nor error", func(t *testing.T) { - var err error + dpl := fixture.Deployment() + assert.Nil(t, dpl.HasStarted()) - dpl, _ := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) - dpl.HasStarted() + err := dpl.HasEnded(nil, nil) - err = dpl.HasEnded(nil, nil) + assert.Nil(t, err) + assert.HasNEvents(t, 3, &dpl, "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, &dpl, 2) - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status()) - testutil.IsTrue(t, evt.State.Services().HasValue()) + assert.Equal(t, dpl.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusSucceeded, evt.State.Status()) + assert.True(t, evt.State.Services().HasValue()) }) t.Run("could be marked has ended with an error", func(t *testing.T) { @@ -132,94 +130,120 @@ func Test_Deployment(t *testing.T) { reason = errors.New("failed reason") ) - dpl, _ := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) - dpl.HasStarted() + dpl := fixture.Deployment() + assert.Nil(t, dpl.HasStarted()) err = dpl.HasEnded(nil, reason) - 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, &dpl, "should have events related to deployment started and ended") + + evt := assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 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, dpl.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusFailed, evt.State.Status()) + assert.Equal(t, reason.Error(), evt.State.ErrCode().MustGet()) + 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_test.go b/internal/deployment/domain/service_test.go index fc751739..092d1ab2 100644 --- a/internal/deployment/domain/service_test.go +++ b/internal/deployment/domain/service_test.go @@ -6,125 +6,127 @@ 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)) t.Run("could be created from a deployment configuration", func(t *testing.T) { - s := config.NewService("db", "postgres:14-alpine") + app := fixture.App(fixture.WithAppName("my-app")) + appidLower := strings.ToLower(string(app.ID())) + deployment := fixture.Deployment(fixture.FromApp(app)) - testutil.Equals(t, "db", s.Name()) - testutil.Equals(t, "postgres:14-alpine", s.Image()) + s := deployment.Config().NewService("db", "postgres:14-alpine") - s = config.NewService("app", "") + assert.Equal(t, "db", s.Name()) + assert.Equal(t, "postgres:14-alpine", s.Image()) - testutil.Equals(t, "app", s.Name()) - testutil.Equals(t, fmt.Sprintf("my-app-%s/app:production", appidLower), s.Image()) + s = deployment.Config().NewService("app", "") + + assert.Equal(t, "app", s.Name()) + assert.Equal(t, fmt.Sprintf("my-app-%s/app:production", appidLower), s.Image()) }) t.Run("should populate the subdomain when adding HTTP entrypoints", func(t *testing.T) { - s := config.NewService("app", "") + app := fixture.App(fixture.WithAppName("my-app")) + appidLower := strings.ToLower(string(app.ID())) + deployment := fixture.Deployment(fixture.FromApp(app)) - e := s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{ + s := deployment.Config().NewService("app", "") + e := s.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{ Managed: true, UseDefaultSubdomain: true, }) - 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) + assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-80-http", appidLower), string(e.Name())) + assert.Equal(t, domain.RouterHttp, e.Router()) + assert.False(t, e.IsCustom()) + assert.Equal(t, "my-app", e.Subdomain().Get("")) + assert.Equal(t, 80, e.Port()) + + e = s.AddHttpEntrypoint(deployment.Config(), 8080, domain.HttpEntrypointOptions{}) + assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-8080-http", appidLower), string(e.Name())) + assert.Equal(t, domain.RouterHttp, e.Router()) + assert.True(t, e.IsCustom()) + assert.Equal(t, "my-app", e.Subdomain().Get("")) + assert.Equal(t, 8080, e.Port()) + + same := s.AddHttpEntrypoint(deployment.Config(), 8080, domain.HttpEntrypointOptions{}) + assert.Equal(t, e, same) }) t.Run("could have one or more TCP/UDP entrypoints attached", func(t *testing.T) { - s := config.NewService("app", "") + app := fixture.App(fixture.WithAppName("my-app")) + appidLower := strings.ToLower(string(app.ID())) + deployment := fixture.Deployment(fixture.FromApp(app)) + s := deployment.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()) + 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 := 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()) + 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()) same := s.AddTCPEntrypoint(8080) - testutil.Equals(t, tcp, same) + assert.Equal(t, tcp, same) same = s.AddUDPEntrypoint(8080) - testutil.Equals(t, udp, same) + assert.Equal(t, udp, same) }) } 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 be able to return all entrypoints", func(t *testing.T) { + deployment := fixture.Deployment() var services domain.Services - s := config.NewService("app", "") - http := s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{ + s := deployment.Config().NewService("app", "") + http := s.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{ Managed: true, }) udp := s.AddUDPEntrypoint(8080) services = append(services, s) - s = config.NewService("db", "postgres:14-alpine") + s = deployment.Config().NewService("db", "postgres:14-alpine") tcp := s.AddTCPEntrypoint(5432) services = append(services, s) - s = config.NewService("cache", "redis:6-alpine") + s = deployment.Config().NewService("cache", "redis:6-alpine") services = append(services, s) entrypoints := services.Entrypoints() - testutil.HasLength(t, entrypoints, 3) - testutil.Equals(t, http, entrypoints[0]) - testutil.Equals(t, udp, entrypoints[1]) - testutil.Equals(t, tcp, entrypoints[2]) + assert.HasLength(t, 3, entrypoints) + assert.Equal(t, http, entrypoints[0]) + assert.Equal(t, udp, entrypoints[1]) + assert.Equal(t, tcp, entrypoints[2]) entrypoints = services.CustomEntrypoints() - testutil.HasLength(t, entrypoints, 2) - testutil.Equals(t, udp, entrypoints[0]) - testutil.Equals(t, tcp, entrypoints[1]) + assert.HasLength(t, 2, entrypoints) + assert.Equal(t, udp, entrypoints[0]) + assert.Equal(t, tcp, entrypoints[1]) }) t.Run("should implement the valuer interface", func(t *testing.T) { var services domain.Services + app := fixture.App(fixture.WithAppName("my-app")) + deployment := fixture.Deployment(fixture.FromApp(app)) + appidLower := strings.ToLower(string(deployment.ID().AppID())) - s := config.NewService("app", "") - s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{ + s := deployment.Config().NewService("app", "") + s.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{ UseDefaultSubdomain: true, Managed: true, }) @@ -132,18 +134,18 @@ func Test_Services(t *testing.T) { services = append(services, s) - s = config.NewService("db", "postgres:14-alpine") + s = deployment.Config().NewService("db", "postgres:14-alpine") s.AddTCPEntrypoint(5432) services = append(services, s) - s = config.NewService("cache", "redis:6-alpine") + s = deployment.Config().NewService("cache", "redis:6-alpine") services = append(services, s) value, err := services.Value() - 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":[]}]`, + assert.Nil(t, err) + assert.Equal(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)) }) @@ -194,13 +196,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","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)) }) } @@ -208,28 +210,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_test.go b/internal/deployment/domain/state_test.go index 39a937f0..75e2e948 100644 --- a/internal/deployment/domain/state_test.go +++ b/internal/deployment/domain/state_test.go @@ -3,21 +3,22 @@ package domain_test import ( "errors" "testing" + "time" "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_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()) + assert.Equal(t, domain.DeploymentStatusPending, state.Status()) + assert.Zero(t, state.ErrCode()) + assert.False(t, state.Services().HasValue()) + assert.Zero(t, state.StartedAt()) + assert.Zero(t, state.FinishedAt()) }) t.Run("could be marked as started", func(t *testing.T) { @@ -28,27 +29,31 @@ func Test_DeploymentState(t *testing.T) { 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()) + assert.Nil(t, err) + assert.Equal(t, domain.DeploymentStatusRunning, state.Status()) + assert.Zero(t, state.ErrCode()) + assert.False(t, state.Services().HasValue()) + assert.NotZero(t, state.StartedAt()) + assert.Zero(t, state.FinishedAt()) }) t.Run("could fail", func(t *testing.T) { var ( - state domain.DeploymentState - err error + state domain.DeploymentState + givenErr = errors.New("some error") + err error ) - testutil.IsNil(t, state.Started()) + assert.Nil(t, state.Started()) - err = state.Failed(errors.New("some error")) + err = state.Failed(givenErr) - 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()) + assert.Nil(t, err) + assert.Equal(t, domain.DeploymentStatusFailed, state.Status()) + assert.Equal(t, givenErr.Error(), state.ErrCode().Get("")) + assert.False(t, state.Services().HasValue()) + assert.NotZero(t, state.StartedAt()) + assert.NotZero(t, state.FinishedAt()) }) t.Run("could succeed", func(t *testing.T) { @@ -57,50 +62,45 @@ func Test_DeploymentState(t *testing.T) { 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)) + deployment := fixture.Deployment() services := domain.Services{ - conf.NewService("app", ""), + deployment.Config().NewService("app", ""), } - testutil.IsNil(t, state.Started()) + assert.Nil(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()) + assert.Nil(t, err) + assert.Equal(t, domain.DeploymentStatusSucceeded, state.Status()) + assert.Zero(t, state.ErrCode()) + assert.DeepEqual(t, services, state.Services().Get(domain.Services{})) + assert.NotZero(t, state.StartedAt()) + assert.NotZero(t, state.FinishedAt()) }) 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()) + assert.Nil(t, state.Started()) err := state.Started() - testutil.ErrorIs(t, domain.ErrNotInPendingState, err) + assert.ErrorIs(t, domain.ErrNotInPendingState, err) }) - t.Run("should err if trying to fail but not in runing state", func(t *testing.T) { + t.Run("should err if trying to fail but not in running state", func(t *testing.T) { var state domain.DeploymentState err := state.Failed(errors.New("an error")) - testutil.ErrorIs(t, domain.ErrNotInRunningState, err) + assert.ErrorIs(t, domain.ErrNotInRunningState, err) }) - t.Run("should err if trying to succeed but not in runing state", func(t *testing.T) { + t.Run("should err if trying to succeed but not in running state", func(t *testing.T) { var state domain.DeploymentState err := state.Succeeded(domain.Services{}) - testutil.ErrorIs(t, domain.ErrNotInRunningState, err) + assert.ErrorIs(t, domain.ErrNotInRunningState, err) }) } @@ -108,10 +108,10 @@ 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()) + assert.Equal(t, domain.TargetStatusConfiguring, state.Status()) + assert.Zero(t, state.Version()) + assert.Zero(t, state.ErrCode()) + assert.Zero(t, state.LastReadyVersion()) }) t.Run("can be reconfigured", func(t *testing.T) { @@ -119,9 +119,10 @@ func Test_TargetState(t *testing.T) { state.Reconfigure() - testutil.Equals(t, domain.TargetStatusConfiguring, state.Status()) - testutil.IsFalse(t, state.Version().IsZero()) - testutil.IsFalse(t, state.ErrCode().HasValue()) + assert.Equal(t, domain.TargetStatusConfiguring, state.Status()) + assert.NotZero(t, state.Version()) + assert.Zero(t, state.ErrCode()) + assert.Zero(t, state.LastReadyVersion()) }) t.Run("could be marked has done and sets the errcode and status appropriately", func(t *testing.T) { @@ -131,39 +132,37 @@ func Test_TargetState(t *testing.T) { ) state.Reconfigure() - testutil.IsTrue(t, state.Configured(state.Version(), errFailed)) + assert.True(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()) + assert.Equal(t, domain.TargetStatusFailed, state.Status()) + assert.NotZero(t, state.Version()) + assert.Equal(t, errFailed.Error(), state.ErrCode().Get("")) + assert.Zero(t, state.LastReadyVersion()) 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()) + assert.True(t, state.Configured(state.Version(), nil)) + assert.Equal(t, domain.TargetStatusReady, state.Status()) + assert.Equal(t, state.Version(), state.LastReadyVersion().Get(time.Time{})) + assert.False(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)) + assert.False(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()) + assert.Equal(t, domain.TargetStatusConfiguring, state.Status()) + assert.Zero(t, state.ErrCode()) + assert.Zero(t, state.LastReadyVersion()) state.Configured(state.Version(), nil) - testutil.IsFalse(t, state.Configured(state.Version(), errors.New("should not happen"))) + assert.False(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()) + assert.Equal(t, domain.TargetStatusReady, state.Status()) + assert.Equal(t, state.Version(), state.LastReadyVersion().Get(time.Time{})) + assert.Zero(t, state.ErrCode()) }) } diff --git a/internal/deployment/domain/target.go b/internal/deployment/domain/target.go index 149a3310..f64cc6bd 100644 --- a/internal/deployment/domain/target.go +++ b/internal/deployment/domain/target.go @@ -28,7 +28,7 @@ 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 ) type ( diff --git a/internal/deployment/domain/target_test.go b/internal/deployment/domain/target_test.go index d17ec11d..95c24b97 100644 --- a/internal/deployment/domain/target_test.go +++ b/internal/deployment/domain/target_test.go @@ -7,185 +7,194 @@ 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) + _, err := domain.NewTarget("target", + domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://my-url.com")), false), + domain.NewProviderConfigRequirement(fixture.ProviderConfig(), true), "uid") + + assert.ErrorIs(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) + _, err := domain.NewTarget("target", + domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://my-url.com")), true), + domain.NewProviderConfigRequirement(fixture.ProviderConfig(), false), "uid") + + assert.ErrorIs(t, domain.ErrConfigAlreadyTaken, err) }) t.Run("should be instantiable", func(t *testing.T) { - target, err := domain.NewTarget(name, urlUnique, configUnique, uid) + url := must.Panic(domain.UrlFrom("http://my-url.com")) + config := fixture.ProviderConfig() - testutil.IsNil(t, err) - testutil.HasNEvents(t, &target, 1) - evt := testutil.EventIs[domain.TargetCreated](t, &target, 0) + target, err := domain.NewTarget("target", + domain.NewTargetUrlRequirement(url, true), + domain.NewProviderConfigRequirement(config, true), + "uid") - 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()) + assert.Nil(t, err) + 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", + Url: url, + 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.NotZero(t, created.State.Version()) }) 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)) + target := fixture.Target(fixture.WithTargetName("old-name")) err := target.Rename("new-name") - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.TargetRenamed](t, &target, 1) - testutil.Equals(t, "new-name", evt.Name) + assert.Nil(t, err) + evt := assert.EventIs[domain.TargetRenamed](t, &target, 1) + + assert.Equal(t, domain.TargetRenamed{ + ID: target.ID(), + Name: "new-name", + }, evt) - testutil.IsNil(t, target.Rename("new-name")) - testutil.HasNEvents(t, &target, 2) + assert.Nil(t, target.Rename("new-name")) + assert.HasNEvents(t, 2, &target, "should have raised the event once") }) t.Run("could not be renamed if delete requested", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + target := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Rename("new-name")) + assert.Nil(t, target.RequestCleanup(false, "uid")) + + assert.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)) + target := fixture.Target() 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, false)) + assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, err) err = target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true)) + assert.Nil(t, err) + evt := assert.EventIs[domain.TargetUrlChanged](t, &target, 1) + assert.Equal(t, newUrl, evt.Url) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.TargetUrlChanged](t, &target, 1) - testutil.Equals(t, newUrl.String(), evt.Url.String()) + evtTargetChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2) + assert.Equal(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status()) - 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) + assert.Nil(t, target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true))) + assert.HasNEvents(t, 3, &target) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) + assert.Nil(t, target.RequestCleanup(false, "uid")) - newUrl := domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://new-url.com")), true) + err := target.HasUrl(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)) + assert.ErrorIs(t, domain.ErrTargetCleanupRequested, err) }) 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)) + target := fixture.Target(fixture.WithProviderConfig(fixture.ProviderConfig(fixture.WithFingerprint("docker")))) - err := target.HasProvider(domain.NewProviderConfigRequirement(dummyProviderConfig{data: "new-config", fingerprint: "new-fingerprint"}, true)) + err := target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(), true)) - testutil.ErrorIs(t, domain.ErrTargetProviderUpdateNotPermitted, err) + assert.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"} + config := fixture.ProviderConfig(fixture.WithFingerprint("docker")) + target := fixture.Target(fixture.WithProviderConfig(config)) + newConfig := fixture.ProviderConfig(fixture.WithFingerprint("docker")) err := target.HasProvider(domain.NewProviderConfigRequirement(newConfig, false)) - testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, err) + assert.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) + assert.Nil(t, err) + evt := assert.EventIs[domain.TargetProviderChanged](t, &target, 1) + assert.Equal(t, newConfig, evt.Provider) - evtTargetChanged := testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.Equals(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status()) + evtTargetChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2) + assert.Equal(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status()) - testutil.IsNil(t, target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true))) - testutil.HasNEvents(t, &target, 3) + assert.Nil(t, target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true))) + assert.HasNEvents(t, 3, &target, "should raise the event only once") }) t.Run("could not have its provider changed if delete requested", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + config := fixture.ProviderConfig(fixture.WithFingerprint("docker")) + target := fixture.Target(fixture.WithProviderConfig(config)) target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.HasProvider(configUnique)) + assert.Nil(t, target.RequestCleanup(false, "uid")) + assert.ErrorIs(t, domain.ErrTargetCleanupRequested, + target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(fixture.WithFingerprint("docker")), true))) }) 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 := fixture.Target() target.Configured(target.CurrentVersion().Add(-1*time.Hour), nil, nil) - testutil.HasNEvents(t, &target, 1) - testutil.EventIs[domain.TargetCreated](t, &target, 0) + assert.HasNEvents(t, 1, &target, "should not raise a new event since the version does not match") + assert.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()) + assert.HasNEvents(t, 2, &target, "should raise the event once") + changed := assert.EventIs[domain.TargetStateChanged](t, &target, 1) + assert.Equal(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)) + target := fixture.Target() + deployment := fixture.Deployment() // Assigning non existing entrypoints should just be ignored target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { + deployment.ID().AppID(): { domain.Production: { "non-existing-entrypoint": 5432, }, }, }, nil) - testutil.HasNEvents(t, &target, 2) - testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) + assert.HasNEvents(t, 2, &target) + assert.DeepEqual(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) - dbService := deployConfig.NewService("db", "postgres:14-alpine") - http := dbService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) + dbService := deployment.Config().NewService("db", "postgres:14-alpine") + http := dbService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{}) tcp := dbService.AddTCPEntrypoint(5432) - target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService}) + target.ExposeEntrypoints(deployment.ID().AppID(), domain.Production, domain.Services{dbService}) // Assigning but with an error should ignore new entrypoints target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { + deployment.ID().AppID(): { domain.Production: { http.Name(): 8081, tcp.Name(): 8082, @@ -193,9 +202,9 @@ func Test_Target(t *testing.T) { }, }, errors.New("some error")) - testutil.HasNEvents(t, &target, 5) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { + assert.HasNEvents(t, 5, &target) + assert.DeepEqual(t, domain.TargetEntrypoints{ + deployment.ID().AppID(): { domain.Production: { http.Name(): monad.None[domain.Port](), tcp.Name(): monad.None[domain.Port](), @@ -203,11 +212,11 @@ func Test_Target(t *testing.T) { }, }, target.CustomEntrypoints()) - testutil.IsNil(t, target.Reconfigure()) + assert.Nil(t, target.Reconfigure()) // No error, should update the entrypoints correctly target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { + deployment.ID().AppID(): { domain.Production: { http.Name(): 8081, tcp.Name(): 8082, @@ -224,12 +233,12 @@ func Test_Target(t *testing.T) { }, }, 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(): { + assert.HasNEvents(t, 8, &target) + assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 6) + changed := assert.EventIs[domain.TargetStateChanged](t, &target, 7) + assert.Equal(t, domain.TargetStatusReady, changed.State.Status()) + assert.DeepEqual(t, domain.TargetEntrypoints{ + deployment.ID().AppID(): { domain.Production: { http.Name(): monad.Value[domain.Port](8081), tcp.Name(): monad.Value[domain.Port](8082), @@ -239,18 +248,19 @@ func Test_Target(t *testing.T) { }) 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{}) + target := fixture.Target() + deployment := fixture.Deployment() + dbService := deployment.Config().NewService("db", "postgres:14-alpine") + http := dbService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{}) tcp := dbService.AddTCPEntrypoint(5432) - target.UnExposeEntrypoints(app.ID()) + target.UnExposeEntrypoints(deployment.ID().AppID()) - testutil.HasNEvents(t, &target, 1) + assert.HasNEvents(t, 1, &target, "should not raise an event since no entrypoints were exposed") - target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService}) + target.ExposeEntrypoints(deployment.ID().AppID(), domain.Production, domain.Services{dbService}) target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { + deployment.ID().AppID(): { domain.Production: { http.Name(): 8081, tcp.Name(): 8082, @@ -258,16 +268,16 @@ func Test_Target(t *testing.T) { }, }, nil) - target.UnExposeEntrypoints(app.ID()) + target.UnExposeEntrypoints(deployment.ID().AppID()) - 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()) + assert.HasNEvents(t, 7, &target) + assert.DeepEqual(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) + changed := assert.EventIs[domain.TargetStateChanged](t, &target, 6) + assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status()) - target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService}) + target.ExposeEntrypoints(deployment.ID().AppID(), domain.Production, domain.Services{dbService}) target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { + deployment.ID().AppID(): { domain.Production: { http.Name(): 8081, tcp.Name(): 8082, @@ -275,243 +285,247 @@ func Test_Target(t *testing.T) { }, }, nil) - target.UnExposeEntrypoints(app.ID(), domain.Staging) - target.UnExposeEntrypoints(app.ID(), domain.Production) + target.UnExposeEntrypoints(deployment.ID().AppID(), domain.Staging) + target.UnExposeEntrypoints(deployment.ID().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, 13, &target) + assert.DeepEqual(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) + changed = assert.EventIs[domain.TargetStateChanged](t, &target, 12) + assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status()) }) t.Run("could expose its availability based on its internal state", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + target := fixture.Target() // Configuring err := target.CheckAvailability() - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + assert.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) + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) // Configuration success - target.Reconfigure() + assert.Nil(t, target.Reconfigure()) target.Configured(target.CurrentVersion(), nil, nil) err = target.CheckAvailability() - testutil.IsNil(t, err) + assert.Nil(t, err) // Delete requested - target.RequestCleanup(false, uid) + assert.Nil(t, target.RequestCleanup(false, "uid")) err = target.CheckAvailability() - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, err) + assert.ErrorIs(t, domain.ErrTargetCleanupRequested, err) }) t.Run("could not be reconfigured if cleanup requested", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + target := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) + assert.Nil(t, target.RequestCleanup(false, "uid")) - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure()) + assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure()) }) t.Run("could not be reconfigured if configuring", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - testutil.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, uid)) + assert.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, "uid")) }) t.Run("should not be removed if configuring", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + target := fixture.Target() - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, uid)) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, "uid")) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - err := target.RequestCleanup(false, uid) - testutil.IsNil(t, err) + err := target.RequestCleanup(false, "uid") + + assert.Nil(t, err) + assert.HasNEvents(t, 3, &target) + evt := assert.EventIs[domain.TargetCleanupRequested](t, &target, 2) - 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.Equal(t, domain.TargetCleanupRequested{ + ID: target.ID(), + Requested: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, evt.Requested.At())), + }, evt) }) - 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)) + t.Run("should not raise an event if the target is already marked has deleting", func(t *testing.T) { + target := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) - testutil.IsNil(t, target.RequestCleanup(false, uid)) + assert.Nil(t, target.RequestCleanup(false, "uid")) + assert.Nil(t, target.RequestCleanup(false, "uid")) - testutil.HasNEvents(t, &target, 3) + assert.HasNEvents(t, 3, &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)) + target := fixture.Target() _, err := target.CleanupStrategy(false) - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) _, err := target.CleanupStrategy(true) - testutil.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) + assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - target.Reconfigure() + assert.Nil(t, target.Reconfigure()) target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) - target.RequestCleanup(false, uid) + assert.Nil(t, target.RequestCleanup(false, "uid")) s, err := target.CleanupStrategy(false) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategySkip, s) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategySkip, s) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) s, err := target.CleanupStrategy(false) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategySkip, s) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategySkip, s) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - target.Reconfigure() + assert.Nil(t, target.Reconfigure()) target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) _, err := target.CleanupStrategy(false) - testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) s, err := target.CleanupStrategy(false) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategyDefault, s) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategyDefault, s) }) 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)) + target := fixture.Target() _, err := target.AppCleanupStrategy(false, true) - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) + assert.Nil(t, target.RequestCleanup(false, "uid")) s, err := target.AppCleanupStrategy(false, false) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategySkip, s) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategySkip, s) }) 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)) + target := fixture.Target() s, err := target.AppCleanupStrategy(false, false) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategySkip, s) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategySkip, s) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - target.Reconfigure() + assert.Nil(t, target.Reconfigure()) target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) _, err := target.AppCleanupStrategy(false, true) - testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) _, err := target.AppCleanupStrategy(true, false) - testutil.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) + assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) }) 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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) s, err := target.AppCleanupStrategy(false, true) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategyDefault, s) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategyDefault, s) }) 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)) + target := fixture.Target() - target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{}) - testutil.HasNEvents(t, &target, 1) + target.ExposeEntrypoints("appid", domain.Production, domain.Services{}) + assert.HasNEvents(t, 1, &target) - target.ExposeEntrypoints(app.ID(), domain.Production, nil) - testutil.HasNEvents(t, &target, 1) + target.ExposeEntrypoints("appid", domain.Production, nil) + assert.HasNEvents(t, 1, &target) }) 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{}) + target := fixture.Target() + deployment := fixture.Deployment() + appService := deployment.Config().NewService("app", "") + http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{}) udp := appService.AddUDPEntrypoint(8080) - dbService := deployConfig.NewService("db", "postgres:14-alpine") + dbService := deployment.Config().NewService("db", "postgres:14-alpine") tcp := dbService.AddTCPEntrypoint(5432) services := domain.Services{appService, dbService} - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), services) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), services) - testutil.HasNEvents(t, &target, 3) - evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 1) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - deployConfig.Environment(): { + assert.HasNEvents(t, 3, &target) + evt := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 1) + assert.DeepEqual(t, domain.TargetEntrypoints{ + deployment.ID().AppID(): { + deployment.Config().Environment(): { http.Name(): monad.None[domain.Port](), udp.Name(): monad.None[domain.Port](), tcp.Name(): monad.None[domain.Port](), @@ -519,43 +533,43 @@ func Test_Target(t *testing.T) { }, }, evt.Entrypoints) - changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status()) + changed := assert.EventIs[domain.TargetStateChanged](t, &target, 2) + assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status()) // Should not trigger it again - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), services) - testutil.HasNEvents(t, &target, 3) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), services) + assert.HasNEvents(t, 3, &target) }) 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", "") - - http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) - - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService}) - - testutil.HasNEvents(t, &target, 3) - evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 1) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - deployConfig.Environment(): { + target := fixture.Target() + deployment := fixture.Deployment() + appService := deployment.Config().NewService("app", "") + http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{}) + + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService}) + + assert.HasNEvents(t, 3, &target) + evt := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 1) + assert.DeepEqual(t, domain.TargetEntrypoints{ + deployment.ID().AppID(): { + deployment.Config().Environment(): { http.Name(): monad.None[domain.Port](), }, }, }, evt.Entrypoints) // Adding a new entrypoint should trigger new events - dbService := deployConfig.NewService("db", "postgres:14-alpine") + dbService := deployment.Config().NewService("db", "postgres:14-alpine") tcp := dbService.AddTCPEntrypoint(5432) - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService}) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().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(): { + assert.HasNEvents(t, 5, &target) + evt = assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3) + assert.DeepEqual(t, domain.TargetEntrypoints{ + deployment.ID().AppID(): { + deployment.Config().Environment(): { http.Name(): monad.None[domain.Port](), tcp.Name(): monad.None[domain.Port](), }, @@ -563,32 +577,32 @@ func Test_Target(t *testing.T) { }, evt.Entrypoints) // 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) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService, dbService, deployment.Config().NewService("cache", "redis:6-alpine")}) + assert.HasNEvents(t, 5, &target) }) 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{}) + target := fixture.Target() + deployment := fixture.Deployment() + appService := deployment.Config().NewService("app", "") + http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{}) appService.AddUDPEntrypoint(8080) - dbService := deployConfig.NewService("db", "postgres:14-alpine") + dbService := deployment.Config().NewService("db", "postgres:14-alpine") tcp := dbService.AddTCPEntrypoint(5432) - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService}) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService, dbService}) // Let's remove the UDP entrypoint - appService = deployConfig.NewService("app", "") - appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) + appService = deployment.Config().NewService("app", "") + appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{}) - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService}) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().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(): { + assert.HasNEvents(t, 5, &target) + evt := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3) + assert.DeepEqual(t, domain.TargetEntrypoints{ + deployment.ID().AppID(): { + deployment.Config().Environment(): { http.Name(): monad.None[domain.Port](), tcp.Name(): monad.None[domain.Port](), }, @@ -597,16 +611,15 @@ func Test_Target(t *testing.T) { }) 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", "") - - http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) + target := fixture.Target() + deployment := fixture.Deployment() + appService := deployment.Config().NewService("app", "") + http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{}) tcp := appService.AddTCPEntrypoint(5432) - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService}) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService}) + assert.DeepEqual(t, domain.TargetEntrypoints{ + deployment.ID().AppID(): { domain.Production: { http.Name(): monad.None[domain.Port](), tcp.Name(): monad.None[domain.Port](), @@ -614,68 +627,52 @@ func Test_Target(t *testing.T) { }, }, target.CustomEntrypoints()) - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{}) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{}) - testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) + assert.DeepEqual(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)) + target := fixture.Target() err := target.Delete(true) - testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err) + assert.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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) // No application is using it + assert.Nil(t, target.RequestCleanup(false, "uid")) // No application is using it err := target.Delete(false) - testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err) + assert.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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) + assert.Nil(t, target.RequestCleanup(false, "uid")) err := target.Delete(true) - testutil.IsNil(t, err) - testutil.EventIs[domain.TargetDeleted](t, &target, 3) + assert.Nil(t, err) + assert.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 := fixture.Target() target.Configured(target.CurrentVersion(), nil, nil) - evt := testutil.EventIs[domain.TargetStateChanged](t, &target, 1) - testutil.IsFalse(t, evt.WentToConfiguringState()) + evt := assert.EventIs[domain.TargetStateChanged](t, &target, 1) + assert.False(t, evt.WentToConfiguringState()) - testutil.IsNil(t, target.Reconfigure()) + assert.Nil(t, target.Reconfigure()) - evt = testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.IsTrue(t, evt.WentToConfiguringState()) + evt = assert.EventIs[domain.TargetStateChanged](t, &target, 2) + assert.True(t, evt.WentToConfiguringState()) }) } - -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/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/deployment.go b/internal/deployment/fixture/deployment.go new file mode 100644 index 00000000..f498c0c1 --- /dev/null +++ b/internal/deployment/fixture/deployment.go @@ -0,0 +1,91 @@ +//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 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/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/target.go b/internal/deployment/fixture/target.go new file mode 100644 index 00000000..66c1f74e --- /dev/null +++ b/internal/deployment/fixture/target.go @@ -0,0 +1,109 @@ +//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 + url domain.Url + provider domain.ProviderConfig + uid auth.UserID + } + + TargetOptionBuilder func(*targetOption) +) + +func Target(options ...TargetOptionBuilder) domain.Target { + opts := targetOption{ + name: id.New[string](), + url: must.Panic(domain.UrlFrom("http://" + id.New[string]() + ".com")), + provider: ProviderConfig(), + uid: id.New[auth.UserID](), + } + + for _, o := range options { + o(&opts) + } + + return must.Panic(domain.NewTarget(opts.name, + domain.NewTargetUrlRequirement(opts.url, true), + 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 WithTargetUrl(url domain.Url) TargetOptionBuilder { + return func(opts *targetOption) { + opts.url = url + } +} + +func WithProviderConfig(config domain.ProviderConfig) TargetOptionBuilder { + return func(opts *targetOption) { + opts.provider = config + } +} + +type ( + providerConfig struct { + Data string + Fingerprint_ string + } + + ProviderConfigBuilder func(*providerConfig) +) + +func ProviderConfig(options ...ProviderConfigBuilder) domain.ProviderConfig { + config := providerConfig{ + Data: id.New[string](), + Fingerprint_: id.New[string](), + } + + for _, o := range options { + o(&config) + } + + return config +} + +func WithFingerprint(fingerprint string) ProviderConfigBuilder { + return func(config *providerConfig) { + config.Fingerprint_ = fingerprint + } +} + +func (d providerConfig) Kind() string { return "test" } +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 +} + +func init() { + domain.ProviderConfigTypes.Register(providerConfig{}, func(s string) (domain.ProviderConfig, error) { + return storage.UnmarshalJSON[providerConfig](s) + }) +} 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/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/provider_test.go b/internal/deployment/infra/provider/docker/provider_test.go index 5febbdee..43e0fd2d 100644 --- a/internal/deployment/infra/provider/docker/provider_test.go +++ b/internal/deployment/infra/provider/docker/provider_test.go @@ -15,16 +15,17 @@ import ( "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,7 +37,7 @@ 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() { @@ -200,14 +201,14 @@ 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)) }) } }) @@ -216,14 +217,14 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID target := createTarget("http://docker.localhost") targetIdLower := strings.ToLower(string(target.ID())) - provider, mock := sut(config.Default(config.WithTestDefaults())) + provider, mock := arrange(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{ + 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": { @@ -271,14 +272,14 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID target := createTarget("https://docker.localhost") targetIdLower := strings.ToLower(string(target.ID())) - provider, mock := sut(config.Default(config.WithTestDefaults())) + provider, mock := arrange(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{ + 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": { @@ -349,21 +350,21 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service}) - provider, mock := sut(config.Default(config.WithTestDefaults())) + provider, mock := arrange(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) + assert.Nil(t, err) + assert.HasLength(t, 2, mock.ups) + assert.HasLength(t, 1, mock.downs) 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) + assert.NotEqual(t, 0, tcpPort) + assert.NotEqual(t, 0, udpPort) - testutil.DeepEquals(t, &types.Project{ + assert.DeepEqual(t, &types.Project{ Name: "seelf-internal-" + targetIdLower, Services: types.Services{ "proxy": { @@ -434,22 +435,22 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID newUdp := service.AddUDPEntrypoint(5435) target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service}) - provider, mock := sut(config.Default(config.WithTestDefaults())) + provider, mock := arrange(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) - testutil.Equals(t, 2, len(assigned[depl.ID().AppID()][depl.Config().Environment()])) + assert.Nil(t, err) + assert.HasLength(t, 2, mock.ups) + assert.HasLength(t, 1, mock.downs) + assert.Equal(t, 2, len(assigned[depl.ID().AppID()][depl.Config().Environment()])) tcpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][newTcp.Name()] udpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][newUdp.Name()] - testutil.NotEquals(t, 0, tcpPort) - testutil.NotEquals(t, 0, udpPort) + assert.NotEqual(t, 0, tcpPort) + assert.NotEqual(t, 0, udpPort) - testutil.DeepEquals(t, &types.Project{ + assert.DeepEqual(t, &types.Project{ Name: "seelf-internal-" + targetIdLower, Services: types.Services{ "proxy": { @@ -501,6 +502,23 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID }, mock.ups[1].project) }) + t.Run("should returns an error if no valid compose file was found for a deployment", func(t *testing.T) { + target := createTarget("http://docker.localhost") + depl := createDeployment(target.ID(), "") + opts := config.Default(config.WithTestDefaults()) + artifactManager := artifact.NewLocal(opts, logger) + + ctx, err := artifactManager.PrepareBuild(context.Background(), depl) + assert.Nil(t, err) + defer ctx.Logger().Close() + + provider, _ := arrange(opts) + + _, err = provider.Deploy(context.Background(), ctx, depl, target, nil) + + assert.ErrorIs(t, docker.ErrOpenComposeFileFailed, err) + }) + t.Run("should expose services from a compose file", func(t *testing.T) { target := createTarget("http://docker.localhost") depl := createDeployment(target.ID(), `services: @@ -543,52 +561,53 @@ volumes: 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)) + assert.Nil(t, err) + assert.Nil(t, raw.New().Fetch(context.Background(), ctx, depl)) + defer ctx.Logger().Close() - provider, mock := sut(opts) + provider, mock := arrange(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) + assert.Nil(t, err) + assert.HasLength(t, 1, mock.ups) + assert.HasLength(t, 3, services) - testutil.Equals(t, "app", services[0].Name()) - testutil.Equals(t, "db", services[1].Name()) - testutil.Equals(t, "sidecar", services[2].Name()) + assert.Equal(t, "app", services[0].Name()) + assert.Equal(t, "db", services[1].Name()) + assert.Equal(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()) + assert.HasLength(t, 4, entrypoints) + assert.Equal(t, 8080, entrypoints[0].Port()) + assert.Equal(t, "http", entrypoints[0].Router()) + assert.Equal(t, string(depl.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(depl.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", 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)) + assert.Equal(t, expectedProjectName, project.Name) + assert.Equal(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{ + 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(depl.ID().AppID()), docker.TargetLabel: string(target.ID()), docker.EnvironmentLabel: string(depl.Config().Environment()), }, service.Labels) - testutil.DeepEquals(t, map[string]*types.ServiceNetworkConfig{ + assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{ "default": nil, }, service.Networks) case "app": @@ -597,9 +616,9 @@ volumes: 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{ + assert.Equal(t, fmt.Sprintf("%s-%s/app:%s", depl.Config().AppName(), appIdLower, depl.Config().Environment()), service.Image) + assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart) + assert.DeepEqual(t, types.Labels{ docker.AppLabel: string(depl.ID().AppID()), docker.TargetLabel: string(target.ID()), docker.EnvironmentLabel: string(depl.Config().Environment()), @@ -616,11 +635,11 @@ volumes: fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", customHttpEntrypointName): "8082", }, service.Labels) - testutil.HasLength(t, service.Ports, 0) - testutil.DeepEquals(t, types.MappingWithEquals{ + assert.HasLength(t, 0, service.Ports) + assert.DeepEqual(t, types.MappingWithEquals{ "DSN": &dsn, }, service.Environment) - testutil.DeepEquals(t, map[string]*types.ServiceNetworkConfig{ + assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{ "default": nil, expectedGatewayNetworkName: nil, }, service.Networks) @@ -629,9 +648,9 @@ volumes: 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{ + assert.Equal(t, "postgres:14-alpine", service.Image) + assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart) + assert.DeepEqual(t, types.Labels{ docker.AppLabel: string(depl.ID().AppID()), docker.TargetLabel: string(target.ID()), docker.EnvironmentLabel: string(depl.Config().Environment()), @@ -641,16 +660,16 @@ volumes: 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{ + assert.HasLength(t, 0, service.Ports) + assert.DeepEqual(t, types.MappingWithEquals{ "POSTGRES_USER": &postgresUser, "POSTGRES_PASSWORD": &postgresPassword, }, service.Environment) - testutil.DeepEquals(t, map[string]*types.ServiceNetworkConfig{ + assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{ "default": nil, expectedGatewayNetworkName: nil, }, service.Networks) - testutil.DeepEquals(t, []types.ServiceVolumeConfig{ + assert.DeepEqual(t, []types.ServiceVolumeConfig{ { Type: types.VolumeTypeVolume, Source: "dbdata", @@ -663,7 +682,7 @@ volumes: } } - testutil.DeepEquals(t, types.Networks{ + assert.DeepEqual(t, types.Networks{ "default": { Name: expectedProjectName + "_default", Labels: types.Labels{ @@ -677,7 +696,7 @@ volumes: External: true, }, }, project.Networks) - testutil.DeepEquals(t, types.Volumes{ + assert.DeepEqual(t, types.Volumes{ "dbdata": { Name: expectedProjectName + "_dbdata", Labels: types.Labels{ @@ -688,7 +707,7 @@ volumes: }, }, project.Volumes) - testutil.DeepEquals(t, filters.NewArgs( + assert.DeepEqual(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())), @@ -822,9 +841,9 @@ 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) { diff --git a/internal/deployment/infra/provider/facade_test.go b/internal/deployment/infra/provider/facade_test.go index 83eb670e..12efd61f 100644 --- a/internal/deployment/infra/provider/facade_test.go +++ b/internal/deployment/infra/provider/facade_test.go @@ -6,8 +6,8 @@ import ( "github.com/YuukanOO/seelf/internal/deployment/domain" "github.com/YuukanOO/seelf/internal/deployment/infra/provider" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Facade(t *testing.T) { @@ -23,7 +23,7 @@ func Test_Facade(t *testing.T) { _, 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) { @@ -31,7 +31,7 @@ func Test_Facade(t *testing.T) { _, 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) { @@ -39,7 +39,7 @@ func Test_Facade(t *testing.T) { _, 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) { @@ -47,7 +47,7 @@ func Test_Facade(t *testing.T) { 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) { @@ -55,7 +55,7 @@ func Test_Facade(t *testing.T) { 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) { @@ -63,7 +63,7 @@ func Test_Facade(t *testing.T) { err := sut.Cleanup(context.Background(), app.ID(), target, domain.Production, domain.CleanupStrategyDefault) - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) } 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/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/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_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..f2beaf40 --- /dev/null +++ b/pkg/bus/spy/dispatcher.go @@ -0,0 +1,51 @@ +//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{ + requests: make([]bus.Request, 0), + signals: make([]bus.Signal, 0), + } +} + +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 = make([]bus.Request, 0) + d.signals = make([]bus.Signal, 0) +} + +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..ae357fd6 100644 --- a/pkg/bus/sqlite/store.go +++ b/pkg/bus/sqlite/store.go @@ -21,7 +21,7 @@ var ( //go:embed migrations/*.sql migrations embed.FS - migrationsModule = sqlite.NewMigrationsModule("scheduler", "migrations", migrations) + Migrations = sqlite.NewMigrationsModule("scheduler", "migrations", migrations) ) type ( @@ -67,7 +67,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 } 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_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,