feat: activity log producers
This commit is contained in:
1 parent
50b1649965
commit
e37a46a027
93 files changed
+3396
-440
No files matched your search
@@ -116,7 +116,7 @@ func newAtomicAcceptFixture(t *testing.T, wrap func(db *gorm.DB, repo invite.Rep
|
||||
hasher := atomicPlainHasher{}
|
||||
handler := auth.NewAcceptInviteHandler(
|
||||
useCases.NewGetInviteUseCase(invites, hasher),
|
||||
useCases.NewAcceptInviteUseCase(users, invites, hasher),
|
||||
useCases.NewAcceptInviteUseCase(users, invites, hasher, discardRecorder),
|
||||
jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -56,20 +56,20 @@ func newAcceptInviteFixture(t *testing.T, maxAttempts int) *acceptInviteFixture
|
||||
getUser := useCases.NewGetUserUseCase(f.users)
|
||||
acceptHandler := auth.NewAcceptInviteHandler(
|
||||
useCases.NewGetInviteUseCase(f.invites, f.hasher),
|
||||
useCases.NewAcceptInviteUseCase(f.users, f.invites, f.hasher),
|
||||
useCases.NewAcceptInviteUseCase(f.users, f.invites, f.hasher, discardRecorder),
|
||||
f.token,
|
||||
true,
|
||||
)
|
||||
meHandler := auth.NewMeHandler(getUser)
|
||||
listHandler := userhandler.NewInviteHandler(
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL),
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
nil,
|
||||
nil,
|
||||
useCases.NewListUsersAndInvitesUseCase(f.users, f.invites),
|
||||
)
|
||||
registerHandler := auth.NewLoginHandler(
|
||||
f.token,
|
||||
useCases.NewCreateUserUseCase(f.users, f.hasher, f.mail, invitePublicURL),
|
||||
useCases.NewCreateUserUseCase(f.users, f.hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(f.users, f.hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
@@ -500,13 +500,13 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
newUseCase := func() (*useCases.AcceptInviteUseCase, *mockUserRepo, *inviteFakeRepo, *stubHasher) {
|
||||
users, invites, hasher := inviteInviterRepo(""), newInviteFakeRepo(), &stubHasher{}
|
||||
invites.users = users
|
||||
return useCases.NewAcceptInviteUseCase(users, invites, hasher), users, invites, hasher
|
||||
return useCases.NewAcceptInviteUseCase(users, invites, hasher, discardRecorder), users, invites, hasher
|
||||
}
|
||||
command := useCases.AcceptInviteCommand{Code: acceptCode, Username: "lea", Password: acceptPassword}
|
||||
|
||||
t.Run("unknown code is invalid", func(t *testing.T) {
|
||||
uc, _, _, _ := newUseCase()
|
||||
if _, err := uc.AcceptInvite(command); !errors.Is(err, invite.ErrInviteInvalid) {
|
||||
if _, err := uc.AcceptInvite(testCtx, command); !errors.Is(err, invite.ErrInviteInvalid) {
|
||||
t.Errorf("expected ErrInviteInvalid, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -514,7 +514,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
t.Run("empty code is invalid without hashing", func(t *testing.T) {
|
||||
uc, _, invites, hasher := newUseCase()
|
||||
invites.seed(pendingInvite(acceptEmail))
|
||||
if _, err := uc.AcceptInvite(useCases.AcceptInviteCommand{Username: "lea", Password: acceptPassword}); !errors.Is(err, invite.ErrInviteInvalid) || hasher.compareCalls != 0 {
|
||||
if _, err := uc.AcceptInvite(testCtx, useCases.AcceptInviteCommand{Username: "lea", Password: acceptPassword}); !errors.Is(err, invite.ErrInviteInvalid) || hasher.compareCalls != 0 {
|
||||
t.Errorf("expected ErrInviteInvalid, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -524,7 +524,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
stored := expiredInvite(acceptEmail)
|
||||
stored.HashedCode = "hashed:" + acceptCode
|
||||
invites.seed(stored)
|
||||
if _, err := uc.AcceptInvite(command); !errors.Is(err, invite.ErrInviteExpired) {
|
||||
if _, err := uc.AcceptInvite(testCtx, command); !errors.Is(err, invite.ErrInviteExpired) {
|
||||
t.Errorf("expected ErrInviteExpired, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -534,7 +534,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
stored := pendingInvite(acceptEmail)
|
||||
stored.HashedCode = "hashed:" + acceptCode
|
||||
invites.seed(stored)
|
||||
_, err := uc.AcceptInvite(useCases.AcceptInviteCommand{Code: acceptCode, Username: "marta", Password: acceptPassword})
|
||||
_, err := uc.AcceptInvite(testCtx, useCases.AcceptInviteCommand{Code: acceptCode, Username: "marta", Password: acceptPassword})
|
||||
if !errors.Is(err, user.ErrUsernameTaken) || len(users.users) != 1 {
|
||||
t.Errorf("expected ErrUsernameTaken, got %v", err)
|
||||
}
|
||||
@@ -546,7 +546,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
stored.HashedCode = "hashed:" + acceptCode
|
||||
invites.seed(stored)
|
||||
users.fail.failNext("GetByUsername")
|
||||
if _, err := uc.AcceptInvite(command); !errors.Is(err, errRepositoryFailure) {
|
||||
if _, err := uc.AcceptInvite(testCtx, command); !errors.Is(err, errRepositoryFailure) {
|
||||
t.Errorf("expected repository failure, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -557,7 +557,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
stored.HashedCode = "hashed:" + acceptCode
|
||||
invites.seed(stored)
|
||||
users.users[8] = user.User{ID: 8, Username: "leam", Email: "LEA.martin@renew.example"}
|
||||
if _, err := uc.AcceptInvite(command); !errors.Is(err, invite.ErrEmailHasAccount) {
|
||||
if _, err := uc.AcceptInvite(testCtx, command); !errors.Is(err, invite.ErrEmailHasAccount) {
|
||||
t.Errorf("expected ErrEmailHasAccount, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -574,8 +574,8 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
invites.invites[seeded.ID] = seeded
|
||||
users.users[8] = user.User{ID: 8, Username: "lea2", Email: acceptEmail}
|
||||
}}
|
||||
uc := useCases.NewAcceptInviteUseCase(users, racing, hasher)
|
||||
if _, err := uc.AcceptInvite(command); !errors.Is(err, invite.ErrInviteInvalid) {
|
||||
uc := useCases.NewAcceptInviteUseCase(users, racing, hasher, discardRecorder)
|
||||
if _, err := uc.AcceptInvite(testCtx, command); !errors.Is(err, invite.ErrInviteInvalid) {
|
||||
t.Errorf("expected ErrInviteInvalid, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -586,7 +586,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
stored.HashedCode = "hashed:" + acceptCode
|
||||
invites.seed(stored)
|
||||
hasher.hashErr = errors.New("hash boom")
|
||||
if _, err := uc.AcceptInvite(command); err == nil || len(users.users) != 1 {
|
||||
if _, err := uc.AcceptInvite(testCtx, command); err == nil || len(users.users) != 1 {
|
||||
t.Errorf("expected failure without a user, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -597,7 +597,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
stored.HashedCode = "hashed:" + acceptCode
|
||||
seeded := invites.seed(stored)
|
||||
users.fail.failNext("Create")
|
||||
if _, err := uc.AcceptInvite(command); !errors.Is(err, errRepositoryFailure) || invites.invites[seeded.ID].AcceptedAt != nil {
|
||||
if _, err := uc.AcceptInvite(testCtx, command); !errors.Is(err, errRepositoryFailure) || invites.invites[seeded.ID].AcceptedAt != nil {
|
||||
t.Errorf("expected repository failure and a pending invite, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -609,7 +609,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
seeded := invites.seed(stored)
|
||||
invites.fail.failNextWith("MarkAccepted", errors.New("mark down"))
|
||||
users.fail.failNextWith("Delete", errors.New("delete down"))
|
||||
_, err := uc.AcceptInvite(command)
|
||||
_, err := uc.AcceptInvite(testCtx, command)
|
||||
if err == nil || !strings.Contains(err.Error(), "mark down") || strings.Contains(err.Error(), "delete down") {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
@@ -624,7 +624,7 @@ func TestAcceptInviteUseCase(t *testing.T) {
|
||||
stored.HashedCode = "hashed:" + acceptCode
|
||||
invites.seed(stored)
|
||||
invites.fail.failNextWith("MarkAccepted", invite.ErrInviteNotFound)
|
||||
if _, err := uc.AcceptInvite(command); !errors.Is(err, invite.ErrInviteInvalid) || len(users.users) != 1 {
|
||||
if _, err := uc.AcceptInvite(testCtx, command); !errors.Is(err, invite.ErrInviteInvalid) || len(users.users) != 1 {
|
||||
t.Errorf("expected ErrInviteInvalid, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
func newAccountVerificationRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher),
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder),
|
||||
useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -16,9 +16,9 @@ func TestAccountVerificationUseCase(t *testing.T) {
|
||||
VerificationCode: "hashed:right-code",
|
||||
VerificationExpiration: time.Now().Add(time.Hour),
|
||||
}
|
||||
uc := useCases.NewVerifyUserUseCase(repo, &stubHasher{})
|
||||
uc := useCases.NewVerifyUserUseCase(repo, &stubHasher{}, discardRecorder)
|
||||
|
||||
_, err := uc.Verify(3, "wrong-code")
|
||||
_, err := uc.Verify(testCtx, 3, "wrong-code")
|
||||
|
||||
if !errors.Is(err, user.ErrInvalidVerificationCode) {
|
||||
t.Fatalf("expected ErrInvalidVerificationCode, got %v", err)
|
||||
@@ -35,9 +35,9 @@ func TestAccountVerificationUseCase(t *testing.T) {
|
||||
VerificationCode: "hashed:right-code",
|
||||
VerificationExpiration: time.Now().Add(-time.Hour),
|
||||
}
|
||||
uc := useCases.NewVerifyUserUseCase(repo, &stubHasher{})
|
||||
uc := useCases.NewVerifyUserUseCase(repo, &stubHasher{}, discardRecorder)
|
||||
|
||||
_, err := uc.Verify(3, "right-code")
|
||||
_, err := uc.Verify(testCtx, 3, "right-code")
|
||||
|
||||
if !errors.Is(err, user.ErrVerificationCodeExpired) {
|
||||
t.Fatalf("expected ErrVerificationCodeExpired, got %v", err)
|
||||
@@ -54,9 +54,9 @@ func TestAccountVerificationUseCase(t *testing.T) {
|
||||
VerificationCode: "hashed:right-code",
|
||||
VerificationExpiration: time.Now().Add(time.Hour),
|
||||
}
|
||||
uc := useCases.NewVerifyUserUseCase(repo, &stubHasher{})
|
||||
uc := useCases.NewVerifyUserUseCase(repo, &stubHasher{}, discardRecorder)
|
||||
|
||||
result, err := uc.Verify(3, "right-code")
|
||||
result, err := uc.Verify(testCtx, 3, "right-code")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
|
||||
@@ -0,0 +1,943 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api"
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/update"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
var producersTimestamps = regexp.MustCompile(`"(created_at|expires_at)":"[^"]*",?`)
|
||||
|
||||
const (
|
||||
producersUserAgent = "producers-test/1.0"
|
||||
producersClientIP = "192.0.2.1"
|
||||
producersOwnerName = "Marta Alves"
|
||||
producersSpringName = "Spring release"
|
||||
producersBannerPath = "/static/uploadedImages/"
|
||||
)
|
||||
|
||||
type producersEnv struct {
|
||||
*apiKeyEnv
|
||||
audit *activityHarness
|
||||
}
|
||||
|
||||
func newProducersEnv(t *testing.T) *producersEnv {
|
||||
t.Helper()
|
||||
h := newActivityHarness(t)
|
||||
env := newAPIKeyEnvRecording(t, h.recorder)
|
||||
owner := env.users.users[apiKeyOwnerId]
|
||||
owner.DisplayName = producersOwnerName
|
||||
env.users.users[apiKeyOwnerId] = owner
|
||||
env.container.ListActivityUseCase = useCases.NewListActivityUseCase(h.store, env.users, env.articles, env.images, env.keys, h.clock)
|
||||
router := chi.NewRouter()
|
||||
router.Mount("/v1", api.InitBackendRoutes(env.container, env.settings))
|
||||
env.router = router
|
||||
return &producersEnv{apiKeyEnv: env, audit: h}
|
||||
}
|
||||
|
||||
func (p *producersEnv) request(t *testing.T, req *http.Request, userId uint32) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req.Header.Set("User-Agent", producersUserAgent)
|
||||
if userId != 0 {
|
||||
req.AddCookie(p.cookie(t, userId, 0))
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
p.router.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func (p *producersEnv) as(t *testing.T, userId uint32, method string, path string, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
if body != "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
return p.request(t, req, userId)
|
||||
}
|
||||
|
||||
func (p *producersEnv) asKey(t *testing.T, raw string, method string, path string, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
if body != "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+raw)
|
||||
return p.request(t, req, 0)
|
||||
}
|
||||
|
||||
func (p *producersEnv) upload(t *testing.T, userId uint32, filename string, contentType string, content []byte) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
return p.request(t, newMultipartFileRequest(t, http.MethodPost, "/v1/media", filename, contentType, content), userId)
|
||||
}
|
||||
|
||||
func (p *producersEnv) events(t *testing.T) []audit.Event {
|
||||
t.Helper()
|
||||
return p.audit.all(t)
|
||||
}
|
||||
|
||||
func (p *producersEnv) single(t *testing.T) audit.Event {
|
||||
t.Helper()
|
||||
events := p.events(t)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected exactly one event, got %d: %+v", len(events), events)
|
||||
}
|
||||
return events[0]
|
||||
}
|
||||
|
||||
func (p *producersEnv) noEvent(t *testing.T) {
|
||||
t.Helper()
|
||||
if events := p.events(t); len(events) != 0 {
|
||||
t.Fatalf("expected no event, got %d: %+v", len(events), events)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *producersEnv) seedSpring(id uint32, body string, online bool) {
|
||||
p.articles.articles[id] = article.Article{ID: id, Title: producersSpringName, Body: body, IsOnline: online}
|
||||
}
|
||||
|
||||
func (p *producersEnv) seedBanner(id uint32, filename string, hash string) {
|
||||
p.images.images[id] = image.Image{ID: id, Filename: filename, ContentType: "image/png", Path: producersBannerPath + strconv.Itoa(int(id)) + ".png", SHA256: hash, UploadedByID: apiKeyOwnerId}
|
||||
}
|
||||
|
||||
func (p *producersEnv) seedPendingInvite(email string) invite.Invite {
|
||||
return p.invites.seed(invite.Invite{Email: email, HashedCode: "hashed:code", ExpiresAt: time.Now().Add(24 * time.Hour), InvitedBy: apiKeyOwnerId, CreatedAt: time.Now()})
|
||||
}
|
||||
|
||||
func paramsJSON(t *testing.T, event audit.Event) string {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(event.Params)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func changesJSON(t *testing.T, event audit.Event) string {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(event.Changes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func assertProducerEvent(t *testing.T, event audit.Event, action string, category audit.Category, outcome audit.Outcome) {
|
||||
t.Helper()
|
||||
if event.Action != action || event.Category != category || event.Outcome != outcome {
|
||||
t.Fatalf("event = %s/%s/%s, expected %s/%s/%s", event.Action, event.Category, event.Outcome, action, category, outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func assertProducerTarget(t *testing.T, event audit.Event, targetType string, id string, name string) {
|
||||
t.Helper()
|
||||
if event.Target != (audit.Target{Type: targetType, ID: id, Name: name}) {
|
||||
t.Errorf("target = %+v, expected {%s %s %s}", event.Target, targetType, id, name)
|
||||
}
|
||||
}
|
||||
|
||||
func assertProducerFailure(t *testing.T, event audit.Event, reason string) {
|
||||
t.Helper()
|
||||
if event.Outcome != audit.OutcomeFailed || event.FailureReason == nil || *event.FailureReason != reason {
|
||||
t.Errorf("outcome = %s, failure_reason = %v, expected failed with %q", event.Outcome, event.FailureReason, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func assertProducerOwnerOrigin(t *testing.T, event audit.Event) {
|
||||
t.Helper()
|
||||
if event.Actor.Type != audit.ActorUser || event.Actor.ID == nil || *event.Actor.ID != apiKeyOwnerId || event.Actor.Name != producersOwnerName {
|
||||
t.Errorf("actor = %+v", event.Actor)
|
||||
}
|
||||
if event.Source != audit.SourceWeb {
|
||||
t.Errorf("source = %s", event.Source)
|
||||
}
|
||||
if event.IP == nil || *event.IP != producersClientIP || event.UserAgent == nil || *event.UserAgent != producersUserAgent {
|
||||
t.Errorf("ip = %v, user agent = %v", event.IP, event.UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityLogProducersRouteContext(t *testing.T) {
|
||||
t.Run("A producing route carries the origin of the request", func(t *testing.T) {
|
||||
t.Run("POST /v1/article with the owner's jwt cookie", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/article", `{"title":"Spring release","body":"Hello"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusCreated)
|
||||
assertProducerOwnerOrigin(t, p.single(t))
|
||||
})
|
||||
|
||||
t.Run("POST /v1/article with an API key Deploy bot", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
raw, keyId := p.issueKey(apiKeyDevId, "Deploy bot")
|
||||
rec := p.asKey(t, raw, http.MethodPost, "/v1/article", `{"title":"Spring release","body":"Hello"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusCreated)
|
||||
event := p.single(t)
|
||||
if event.Actor.Type != audit.ActorAPIKey || event.Actor.ID == nil || *event.Actor.ID != keyId || event.Actor.Name != "Deploy bot" || event.Source != audit.SourceAPIKey {
|
||||
t.Errorf("actor = %+v, source = %s", event.Actor, event.Source)
|
||||
}
|
||||
if event.IP == nil || *event.IP != producersClientIP || event.UserAgent == nil || *event.UserAgent != producersUserAgent {
|
||||
t.Errorf("ip = %v, user agent = %v", event.IP, event.UserAgent)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("POST /v1/media with an API key Deploy bot", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
raw, keyId := p.issueKey(apiKeyDevId, "Deploy bot")
|
||||
req := newMultipartFileRequest(t, http.MethodPost, "/v1/media", "spring-banner.png", "image/png", pngBytes(t, 3, 2))
|
||||
req.Header.Set("Authorization", "Bearer "+raw)
|
||||
rec := p.request(t, req, 0)
|
||||
assertAPIKeyStatus(t, rec, http.StatusCreated)
|
||||
event := p.single(t)
|
||||
if event.Actor.Type != audit.ActorAPIKey || event.Actor.ID == nil || *event.Actor.ID != keyId || event.Actor.Name != "Deploy bot" || event.Source != audit.SourceAPIKey {
|
||||
t.Errorf("actor = %+v, source = %s", event.Actor, event.Source)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("POST /v1/system/update/check with the owner's jwt cookie", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/system/update/check", "")
|
||||
assertAPIKeyStatus(t, rec, http.StatusOK)
|
||||
assertProducerOwnerOrigin(t, p.single(t))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersFailedAuditWrite(t *testing.T) {
|
||||
type row struct {
|
||||
name string
|
||||
panics bool
|
||||
setup func(p *producersEnv)
|
||||
send func(t *testing.T, p *producersEnv) *httptest.ResponseRecorder
|
||||
applied func(t *testing.T, p *producersEnv)
|
||||
}
|
||||
owner := func(method string, path string, body string) func(t *testing.T, p *producersEnv) *httptest.ResponseRecorder {
|
||||
return func(t *testing.T, p *producersEnv) *httptest.ResponseRecorder {
|
||||
return p.as(t, apiKeyOwnerId, method, path, body)
|
||||
}
|
||||
}
|
||||
seedArticle := func(p *producersEnv) { p.seedSpring(8, "Hello", false) }
|
||||
seedCopies := func(p *producersEnv) {
|
||||
p.seedBanner(5, "banner.png", "same")
|
||||
p.seedBanner(6, "banner-copy.png", "same")
|
||||
p.articles.articles[1] = article.Article{ID: 1, Title: "Uses copy", Body: "see 6.png"}
|
||||
}
|
||||
rows := []row{
|
||||
{name: "fails POST /v1/article", setup: func(*producersEnv) {}, send: owner(http.MethodPost, "/v1/article", `{"title":"Spring release","body":"Hello"}`), applied: func(t *testing.T, p *producersEnv) {
|
||||
if len(p.articles.articles) != 1 {
|
||||
t.Errorf("the article was not created")
|
||||
}
|
||||
}},
|
||||
{name: "fails DELETE /v1/article/8", setup: seedArticle, send: owner(http.MethodDelete, "/v1/article/8", ""), applied: func(t *testing.T, p *producersEnv) {
|
||||
if _, found := p.articles.articles[8]; found {
|
||||
t.Errorf("the article was not deleted")
|
||||
}
|
||||
}},
|
||||
{name: "panics PATCH /v1/article/8/publish", panics: true, setup: seedArticle, send: owner(http.MethodPatch, "/v1/article/8/publish", ""), applied: func(t *testing.T, p *producersEnv) {
|
||||
if !p.articles.articles[8].IsOnline {
|
||||
t.Errorf("the article was not published")
|
||||
}
|
||||
}},
|
||||
{name: "fails POST /v1/media", setup: func(*producersEnv) {}, send: func(t *testing.T, p *producersEnv) *httptest.ResponseRecorder {
|
||||
return p.upload(t, apiKeyOwnerId, "spring-banner.png", "image/png", pngBytes(t, 3, 2))
|
||||
}, applied: func(t *testing.T, p *producersEnv) {
|
||||
if len(p.images.images) != 1 {
|
||||
t.Errorf("the image was not stored")
|
||||
}
|
||||
}},
|
||||
{name: "fails POST /v1/media/merge", setup: seedCopies, send: owner(http.MethodPost, "/v1/media/merge", `{"keep_id":5,"merge_ids":[6]}`), applied: func(t *testing.T, p *producersEnv) {
|
||||
if _, found := p.images.images[6]; found {
|
||||
t.Errorf("the copy was not merged")
|
||||
}
|
||||
}},
|
||||
{name: "fails POST /v1/user/invite", setup: func(*producersEnv) {}, send: owner(http.MethodPost, "/v1/user/invite", `{"email":"new@example.com"}`), applied: func(t *testing.T, p *producersEnv) {
|
||||
if len(p.invites.invites) != 1 {
|
||||
t.Errorf("the invite was not created")
|
||||
}
|
||||
}},
|
||||
{name: "fails POST /v1/system/update/restart", setup: func(*producersEnv) {}, send: owner(http.MethodPost, "/v1/system/update/restart", ""), applied: func(*testing.T, *producersEnv) {}},
|
||||
}
|
||||
|
||||
t.Run("A failing or panicking repository leaves the response untouched", func(t *testing.T) {
|
||||
for _, example := range rows {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
healthy := newProducersEnv(t)
|
||||
example.setup(healthy)
|
||||
expected := example.send(t, healthy)
|
||||
|
||||
p := newProducersEnv(t)
|
||||
example.setup(p)
|
||||
if example.panics {
|
||||
p.audit.probe.appendPanic = true
|
||||
} else {
|
||||
p.audit.probe.appendErr = errDiskFull
|
||||
}
|
||||
rec := example.send(t, p)
|
||||
|
||||
if rec.Code != expected.Code || producersTimestamps.ReplaceAllString(rec.Body.String(), "") != producersTimestamps.ReplaceAllString(expected.Body.String(), "") {
|
||||
t.Errorf("response = %d %q, healthy response = %d %q", rec.Code, rec.Body.String(), expected.Code, expected.Body.String())
|
||||
}
|
||||
if rec.Code >= http.StatusBadRequest {
|
||||
t.Fatalf("the request was expected to succeed, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
example.applied(t, p)
|
||||
if !strings.Contains(p.audit.logs.String(), "audit:") {
|
||||
t.Errorf("the process log does not name the failed audit write: %q", p.audit.logs.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersArticles(t *testing.T) {
|
||||
t.Run("Creating an article is recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/article", `{"title":"Spring release","body":"Hello"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusCreated)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "article.create", audit.CategoryArticles, audit.OutcomeSuccess)
|
||||
assertProducerOwnerOrigin(t, event)
|
||||
assertProducerTarget(t, event, "article", "1", producersSpringName)
|
||||
if event.FailureReason != nil || len(event.Changes) != 0 {
|
||||
t.Errorf("failure reason = %v, changes = %+v", event.FailureReason, event.Changes)
|
||||
}
|
||||
if encoded, _ := json.Marshal(event); strings.Contains(string(encoded), "Hello") {
|
||||
t.Errorf("the stored event holds the article body: %s", encoded)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Editing the body records a size delta and never the text", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, strings.Repeat("a", 100), false)
|
||||
newBody := strings.Repeat("secretbody", 41) + "xx"
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/8", `{"body":"`+newBody+`"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "article.edit", audit.CategoryArticles, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "article", "8", producersSpringName)
|
||||
if got := changesJSON(t, event); got != `[{"field":"body","kind":"body","delta":312}]` {
|
||||
t.Errorf("changes = %s", got)
|
||||
}
|
||||
listing := p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/activity", "")
|
||||
assertAPIKeyStatus(t, listing, http.StatusOK)
|
||||
if strings.Contains(listing.Body.String(), "secretbody") || strings.Contains(listing.Body.String(), "aaaa") {
|
||||
t.Errorf("the activity response holds article text: %s", listing.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Saving an identical body records nothing", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, "Same text", false)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/8", `{"body":"Same text"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusOK)
|
||||
p.noEvent(t)
|
||||
})
|
||||
|
||||
t.Run("Publishing is recorded with the publish state", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, "Hello", false)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/8/publish", ""), http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "article.publish", audit.CategoryArticles, audit.OutcomeSuccess)
|
||||
if got := changesJSON(t, event); got != `[{"field":"online","kind":"text","before":"false","after":"true"}]` {
|
||||
t.Errorf("changes = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unpublishing is recorded with the publish state", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, "Hello", true)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/8/unpublish", ""), http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "article.unpublish", audit.CategoryArticles, audit.OutcomeSuccess)
|
||||
if got := changesJSON(t, event); got != `[{"field":"online","kind":"text","before":"true","after":"false"}]` {
|
||||
t.Errorf("changes = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A publish state that does not change records nothing", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
state bool
|
||||
path string
|
||||
status int
|
||||
}{{true, "publish", http.StatusOK}, {false, "unpublish", http.StatusOK}} {
|
||||
t.Run(strconv.FormatBool(example.state)+" "+example.path, func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, "Hello", example.state)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/8/"+example.path, ""), example.status)
|
||||
p.noEvent(t)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Deleting an article is recorded with its name snapshot", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, "Hello", false)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodDelete, "/v1/article/8", ""), http.StatusNoContent)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "article.delete", audit.CategoryArticles, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "article", "8", producersSpringName)
|
||||
|
||||
listing := p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/activity", "")
|
||||
var page struct {
|
||||
Items []struct {
|
||||
Target struct {
|
||||
Exists bool `json:"exists"`
|
||||
} `json:"target"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(listing.Body.Bytes(), &page); err != nil || len(page.Items) != 1 || page.Items[0].Target.Exists {
|
||||
t.Errorf("listing = %s, err = %v", listing.Body.String(), err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A refused article action is recorded as failed with a plain reason", func(t *testing.T) {
|
||||
t.Run("the repository fails with db is locked on create", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.articles.fail.failNextWith("Create", errors.New("db is locked"))
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/article", `{"title":"Spring release","body":"Hello"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusInternalServerError)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "article.create", audit.CategoryArticles, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, useCases.GenericFailureReason)
|
||||
if strings.Contains(paramsJSON(t, event), "locked") || strings.Contains(*event.FailureReason, "locked") {
|
||||
t.Errorf("the raw error leaked: %+v", event)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the repository fails with db is locked on publish", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, "Hello", false)
|
||||
p.articles.fail.failNextWith("UpdateIsOnline", errors.New("db is locked"))
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/8/publish", ""), http.StatusInternalServerError)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "article.publish", audit.CategoryArticles, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, useCases.GenericFailureReason)
|
||||
assertProducerTarget(t, event, "article", "8", producersSpringName)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("An article that does not exist records nothing", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
for _, call := range []struct{ method, path, body string }{
|
||||
{http.MethodDelete, "/v1/article/99", ""},
|
||||
{http.MethodPatch, "/v1/article/99", `{"body":"x"}`},
|
||||
{http.MethodPatch, "/v1/article/99/publish", ""},
|
||||
{http.MethodPatch, "/v1/article/99/unpublish", ""},
|
||||
} {
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, call.method, call.path, call.body), http.StatusNotFound)
|
||||
}
|
||||
p.noEvent(t)
|
||||
})
|
||||
|
||||
t.Run("A request that fails validation records nothing", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/article", `{}`), http.StatusBadRequest)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/abc", `{"body":"x"}`), http.StatusBadRequest)
|
||||
p.noEvent(t)
|
||||
})
|
||||
|
||||
t.Run("Reads are never recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedSpring(8, "Hello", true)
|
||||
for _, path := range []string{"/v1/article", "/v1/article/8"} {
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodGet, path, ""), http.StatusOK)
|
||||
}
|
||||
assertAPIKeyStatus(t, p.as(t, 0, http.MethodGet, "/v1/article/8", ""), http.StatusOK)
|
||||
p.noEvent(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersMedia(t *testing.T) {
|
||||
t.Run("Uploading an image is recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.upload(t, apiKeyOwnerId, "spring-banner.png", "image/png", pngBytes(t, 3, 2)), http.StatusCreated)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.upload", audit.CategoryMedia, audit.OutcomeSuccess)
|
||||
assertProducerOwnerOrigin(t, event)
|
||||
assertProducerTarget(t, event, "image", "1", "spring-banner.png")
|
||||
if encoded, _ := json.Marshal(event); strings.Contains(string(encoded), "/static/") {
|
||||
t.Errorf("the stored event holds a storage path: %s", encoded)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A refused upload is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
contentType string
|
||||
content func(t *testing.T) []byte
|
||||
reason string
|
||||
}{
|
||||
{"a GIF", "image/gif", func(t *testing.T) []byte { return []byte("GIF89a") }, "The file must be a PNG, JPEG, WEBP or SVG image."},
|
||||
{"a PNG larger than the limit", "image/png", func(t *testing.T) []byte { return paddedPngBytes(t, int(image.MaxImageSize)+10) }, "The file is too large to upload."},
|
||||
{"a corrupt PNG", "image/png", func(t *testing.T) []byte { return []byte("not a png") }, "The image could not be read."},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
rec := p.upload(t, apiKeyOwnerId, "spring-banner.png", example.contentType, example.content(t))
|
||||
if rec.Code < http.StatusBadRequest {
|
||||
t.Fatalf("expected a refusal, got %d", rec.Code)
|
||||
}
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.upload", audit.CategoryMedia, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
if event.Target.Name != "spring-banner.png" {
|
||||
t.Errorf("target = %+v", event.Target)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Renaming an image records the filename diff", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "h")
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/media/5", `{"filename":"spring-banner"}`), http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.rename", audit.CategoryMedia, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "image", "5", "spring-banner.png")
|
||||
if got := changesJSON(t, event); got != `[{"field":"filename","kind":"text","before":"banner.png","after":"spring-banner.png"}]` {
|
||||
t.Errorf("changes = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Renaming to the same name records nothing", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "h")
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/media/5", `{"filename":"banner"}`), http.StatusOK)
|
||||
p.noEvent(t)
|
||||
})
|
||||
|
||||
t.Run("A refused rename is recorded as failed", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "h")
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/media/5", `{"filename":"not/valid"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusBadRequest)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.rename", audit.CategoryMedia, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, "That file name is not valid.")
|
||||
assertProducerTarget(t, event, "image", "5", "banner.png")
|
||||
})
|
||||
|
||||
t.Run("Deleting an image is recorded with its name snapshot", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "h")
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodDelete, "/v1/media/5", ""), http.StatusNoContent)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.delete", audit.CategoryMedia, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "image", "5", "banner.png")
|
||||
})
|
||||
|
||||
t.Run("Merging images is recorded once with its counts", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "same")
|
||||
p.seedBanner(6, "banner-copy.png", "same")
|
||||
p.seedBanner(7, "banner-copy-2.png", "same")
|
||||
p.articles.articles[1] = article.Article{ID: 1, Title: "One", Body: "see 6.png"}
|
||||
p.articles.articles[2] = article.Article{ID: 2, Title: "Two", Body: "see 7.png"}
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/media/merge", `{"keep_id":5,"merge_ids":[6,7]}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.merge", audit.CategoryMedia, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "image", "5", "banner.png")
|
||||
if got := paramsJSON(t, event); got != `{"articles_updated":2,"merged":2}` {
|
||||
t.Errorf("params = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A refused merge is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
setup func(p *producersEnv)
|
||||
body string
|
||||
reason string
|
||||
}{
|
||||
{"ErrInvalidMergeRequest", func(p *producersEnv) { p.seedBanner(5, "banner.png", "same") }, `{"keep_id":5,"merge_ids":[5]}`, "The merge request is not valid."},
|
||||
{"ErrMergeCopyMissing", func(p *producersEnv) { p.seedBanner(5, "banner.png", "same") }, `{"keep_id":5,"merge_ids":[6]}`, "One of the copies no longer exists."},
|
||||
{"ErrMergeHashMismatch", func(p *producersEnv) {
|
||||
p.seedBanner(5, "banner.png", "same")
|
||||
p.seedBanner(6, "other.png", "different")
|
||||
}, `{"keep_id":5,"merge_ids":[6]}`, "The images are not identical copies."},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
example.setup(p)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/media/merge", example.body)
|
||||
if rec.Code < http.StatusBadRequest {
|
||||
t.Fatalf("expected a refusal, got %d", rec.Code)
|
||||
}
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.merge", audit.CategoryMedia, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A manual purge is recorded once and not as separate merges", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
seedTwoGroupPurgePlan(p)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/media/purge", "")
|
||||
assertAPIKeyStatus(t, rec, http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.purge", audit.CategoryMedia, audit.OutcomeSuccess)
|
||||
assertProducerOwnerOrigin(t, event)
|
||||
if got := paramsJSON(t, event); got != `{"articles_updated":5,"merged":3}` {
|
||||
t.Errorf("params = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A partly failed manual purge is a success with the failed group count", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
seedThreeGroupPurgePlan(p, 5)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/media/purge", ""), http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.purge", audit.CategoryMedia, audit.OutcomeSuccess)
|
||||
if !strings.Contains(paramsJSON(t, event), `"failed_groups":1`) {
|
||||
t.Errorf("params = %s", paramsJSON(t, event))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A refused manual purge is recorded as failed", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
seedTwoGroupPurgePlan(p)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/media/purge", `{"fingerprint":"stale"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusConflict)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "media.purge", audit.CategoryMedia, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, "The library changed since the preview. Review it again.")
|
||||
})
|
||||
|
||||
t.Run("Browsing and the media usage lookup are never recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "h")
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/media", ""), http.StatusOK)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/media/5/usage", ""), http.StatusOK)
|
||||
p.noEvent(t)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *producersEnv) seedArticleUsing(id uint32, body string) {
|
||||
p.articles.articles[id] = article.Article{ID: id, Title: "Article " + strconv.Itoa(int(id)), Body: body}
|
||||
}
|
||||
|
||||
func seedTwoGroupPurgePlan(p *producersEnv) {
|
||||
p.seedBanner(1, "kept-a.png", "hash-a")
|
||||
p.seedBanner(2, "copy-a1.png", "hash-a")
|
||||
p.seedBanner(3, "copy-a2.png", "hash-a")
|
||||
p.seedBanner(4, "kept-b.png", "hash-b")
|
||||
p.seedBanner(5, "copy-b1.png", "hash-b")
|
||||
for id, body := range map[uint32]string{1: "uses 2.png", 2: "uses 3.png", 3: "uses 5.png", 4: "uses 2.png", 5: "uses 3.png", 6: "1.png 4.png", 7: "1.png 4.png", 8: "1.png 4.png"} {
|
||||
p.seedArticleUsing(id, body)
|
||||
}
|
||||
}
|
||||
|
||||
func seedThreeGroupPurgePlan(p *producersEnv, failingCopy uint32) {
|
||||
p.seedBanner(1, "kept-a.png", "hash-a")
|
||||
p.seedBanner(2, "copy-a.png", "hash-a")
|
||||
p.seedBanner(4, "kept-b.png", "hash-b")
|
||||
p.seedBanner(5, "copy-b.png", "hash-b")
|
||||
p.seedBanner(7, "kept-c.png", "hash-c")
|
||||
p.seedBanner(8, "copy-c.png", "hash-c")
|
||||
for id, body := range map[uint32]string{1: "uses 2.png", 2: "uses 5.png", 3: "uses 8.png", 4: "1.png 4.png 7.png", 5: "1.png 4.png 7.png"} {
|
||||
p.seedArticleUsing(id, body)
|
||||
}
|
||||
p.images.failDeleteFor = map[uint32]error{failingCopy: errors.New("storage offline")}
|
||||
}
|
||||
|
||||
func TestActivityLogProducersUsers(t *testing.T) {
|
||||
t.Run("Changing the display name records a before and after", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/user/me", `{"display_name":"Marta Alves 2"}`), http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "user.update_profile", audit.CategoryUsers, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "user", "1", "Marta Alves 2")
|
||||
if got := changesJSON(t, event); got != `[{"field":"display_name","kind":"text","before":"Marta Alves","after":"Marta Alves 2"}]` {
|
||||
t.Errorf("changes = %s", got)
|
||||
}
|
||||
if event.Actor.Name != producersOwnerName {
|
||||
t.Errorf("the actor snapshot name = %q, expected the name held when the request began", event.Actor.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Setting a display name that does not change records nothing", func(t *testing.T) {
|
||||
for _, sent := range []string{"Marta Alves", " Marta Alves "} {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/user/me", `{"display_name":"`+sent+`"}`), http.StatusOK)
|
||||
p.noEvent(t)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A display name that is too long is recorded as failed", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/user/me", `{"display_name":"`+strings.Repeat("a", 61)+`"}`)
|
||||
assertAPIKeyStatus(t, rec, http.StatusBadRequest)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "user.update_profile", audit.CategoryUsers, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, "That display name is too long.")
|
||||
if len(event.Changes) != 0 {
|
||||
t.Errorf("changes = %+v", event.Changes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Creating an invite is recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/user/invite", `{"email":"new@example.com"}`), http.StatusCreated)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "invite.create", audit.CategoryUsers, audit.OutcomeSuccess)
|
||||
assertProducerOwnerOrigin(t, event)
|
||||
assertProducerTarget(t, event, "invite", "1", "new@example.com")
|
||||
if encoded, _ := json.Marshal(event); strings.Contains(string(encoded), "hashed:") {
|
||||
t.Errorf("the stored event holds a code hash: %s", encoded)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A refused invite action is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
setup func(p *producersEnv)
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
action string
|
||||
reason string
|
||||
}{
|
||||
{"ErrEmailHasAccount", func(*producersEnv) {}, http.MethodPost, "/v1/user/invite", `{"email":"dev@example.com"}`, "invite.create", "That email already has an account."},
|
||||
{"ErrPendingInvite", func(p *producersEnv) { p.seedPendingInvite("new@example.com") }, http.MethodPost, "/v1/user/invite", `{"email":"new@example.com"}`, "invite.create", "An invitation is already pending for that email."},
|
||||
{"ErrInviteEmailNotSent on create", func(p *producersEnv) { p.mail.fail.failNext("Send") }, http.MethodPost, "/v1/user/invite", `{"email":"new@example.com"}`, "invite.create", "The invitation email could not be sent."},
|
||||
{"ErrInviteEmailNotSent on renew", func(p *producersEnv) { p.seedPendingInvite("new@example.com"); p.mail.fail.failNext("Send") }, http.MethodPost, "/v1/user/invite/1/renew", "", "invite.renew", "The invitation email could not be sent."},
|
||||
{"ErrInviteAlreadyAccepted on renew", func(p *producersEnv) { p.seedAcceptedInvite("new@example.com") }, http.MethodPost, "/v1/user/invite/1/renew", "", "invite.renew", "That invitation was already accepted."},
|
||||
{"ErrInviteAlreadyAccepted on cancel", func(p *producersEnv) { p.seedAcceptedInvite("new@example.com") }, http.MethodDelete, "/v1/user/invite/1", "", "invite.cancel", "That invitation was already accepted."},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
example.setup(p)
|
||||
rec := p.as(t, apiKeyOwnerId, example.method, example.path, example.body)
|
||||
if rec.Code < http.StatusBadRequest {
|
||||
t.Fatalf("expected a refusal, got %d", rec.Code)
|
||||
}
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, example.action, audit.CategoryUsers, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cancelling an invite is recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedPendingInvite("new@example.com")
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodDelete, "/v1/user/invite/1", ""), http.StatusNoContent)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "invite.cancel", audit.CategoryUsers, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "invite", "1", "new@example.com")
|
||||
})
|
||||
|
||||
t.Run("Renewing an invite is recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedPendingInvite("new@example.com")
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/user/invite/1/renew", ""), http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "invite.renew", audit.CategoryUsers, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "invite", "1", "new@example.com")
|
||||
})
|
||||
|
||||
t.Run("An invite or user that does not exist records nothing", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodDelete, "/v1/user/invite/99", ""), http.StatusNotFound)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/user/invite/99/renew", ""), http.StatusNotFound)
|
||||
p.noEvent(t)
|
||||
})
|
||||
|
||||
t.Run("Reads and checks are never recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
for _, path := range []string{"/v1/user", "/v1/auth/me", "/v1/auth/registration"} {
|
||||
rec := p.as(t, apiKeyOwnerId, http.MethodGet, path, "")
|
||||
assertAPIKeyStatus(t, rec, http.StatusOK)
|
||||
}
|
||||
p.noEvent(t)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *producersEnv) seedAcceptedInvite(email string) {
|
||||
accepted := time.Now().Add(-time.Hour)
|
||||
p.invites.seed(invite.Invite{Email: email, HashedCode: "hashed:code", ExpiresAt: time.Now().Add(24 * time.Hour), InvitedBy: apiKeyOwnerId, AcceptedAt: &accepted, CreatedAt: time.Now().Add(-2 * time.Hour)})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersSystem(t *testing.T) {
|
||||
t.Run("Checking for an update is recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/system/update/check", ""), http.StatusOK)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "system.update_checked", audit.CategorySystem, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "release", "v2.3.0-beta", "v2.3.0-beta")
|
||||
if got := paramsJSON(t, event); got != `{"update_available":true}` {
|
||||
t.Errorf("params = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A failed update check is recorded with a plain reason", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
err error
|
||||
reason string
|
||||
}{
|
||||
{"ErrFeedUnavailable", update.ErrFeedUnavailable, "The release feed could not be reached."},
|
||||
{"ErrNoRelease", update.ErrNoRelease, "No release was found."},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.feed.err = example.err
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/system/update/check", ""), http.StatusInternalServerError)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "system.update_checked", audit.CategorySystem, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
if event.Target != (audit.Target{}) {
|
||||
t.Errorf("target = %+v", event.Target)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The status poll is never recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/system/update", ""), http.StatusOK)
|
||||
p.noEvent(t)
|
||||
})
|
||||
|
||||
t.Run("A restart request is recorded", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/system/update/restart", ""), http.StatusAccepted)
|
||||
event := p.single(t)
|
||||
assertProducerEvent(t, event, "system.restart_requested", audit.CategorySystem, audit.OutcomeSuccess)
|
||||
assertProducerOwnerOrigin(t, event)
|
||||
if got := paramsJSON(t, event); got != `{"kind":"restart"}` {
|
||||
t.Errorf("params = %s", got)
|
||||
}
|
||||
if event.Target != (audit.Target{}) {
|
||||
t.Errorf("target = %+v", event.Target)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *producersEnv) listedActions(t *testing.T, category string) []string {
|
||||
t.Helper()
|
||||
listing := p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/activity?category="+category, "")
|
||||
assertAPIKeyStatus(t, listing, http.StatusOK)
|
||||
var page struct {
|
||||
Items []struct {
|
||||
Action string `json:"action"`
|
||||
Category string `json:"category"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(listing.Body.Bytes(), &page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actions := []string{}
|
||||
for _, item := range page.Items {
|
||||
if item.Category != category {
|
||||
t.Errorf("item category = %q, expected %q", item.Category, category)
|
||||
}
|
||||
actions = append(actions, item.Action)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
func TestActivityLogProducersReadBackOtherCategories(t *testing.T) {
|
||||
t.Run("Media events are listed under their category", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "same")
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/media/5", `{"filename":"spring"}`), http.StatusOK)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodDelete, "/v1/media/5", ""), http.StatusNoContent)
|
||||
if got := strings.Join(p.listedActions(t, "media"), ","); got != "media.delete,media.rename" {
|
||||
t.Errorf("actions = %s", got)
|
||||
}
|
||||
if got := p.listedActions(t, "articles"); len(got) != 0 {
|
||||
t.Errorf("articles actions = %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User events are listed under their category", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/user/me", `{"display_name":"Marta Alves 2"}`), http.StatusOK)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/user/invite", `{"email":"new@example.com"}`), http.StatusCreated)
|
||||
if got := strings.Join(p.listedActions(t, "users"), ","); got != "invite.create,user.update_profile" {
|
||||
t.Errorf("actions = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("System events are listed under their category", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/system/update/check", ""), http.StatusOK)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/system/update/restart", ""), http.StatusAccepted)
|
||||
if got := strings.Join(p.listedActions(t, "system"), ","); got != "system.restart_requested,system.update_checked" {
|
||||
t.Errorf("actions = %s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersReadBack(t *testing.T) {
|
||||
t.Run("Producer events are listed with their documented shape", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/article", `{"title":"Spring release","body":"Hello"}`), http.StatusCreated)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/1", `{"body":"Hello world"}`), http.StatusOK)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/article/1/publish", ""), http.StatusOK)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodDelete, "/v1/article/1", ""), http.StatusNoContent)
|
||||
|
||||
listing := p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/activity?category=articles", "")
|
||||
assertAPIKeyStatus(t, listing, http.StatusOK)
|
||||
var page struct {
|
||||
Items []struct {
|
||||
Action string `json:"action"`
|
||||
Category string `json:"category"`
|
||||
Target struct {
|
||||
Exists bool `json:"exists"`
|
||||
} `json:"target"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(listing.Body.Bytes(), &page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var actions []string
|
||||
for _, item := range page.Items {
|
||||
actions = append(actions, item.Action)
|
||||
if item.Category != "articles" || item.Target.Exists {
|
||||
t.Errorf("item = %+v", item)
|
||||
}
|
||||
}
|
||||
if strings.Join(actions, ",") != "article.delete,article.publish,article.edit,article.create" {
|
||||
t.Errorf("actions = %v", actions)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A failed producer event is filterable", func(t *testing.T) {
|
||||
p := newProducersEnv(t)
|
||||
p.seedBanner(5, "banner.png", "same")
|
||||
p.as(t, apiKeyOwnerId, http.MethodPost, "/v1/media/merge", `{"keep_id":5,"merge_ids":[6]}`)
|
||||
assertAPIKeyStatus(t, p.as(t, apiKeyOwnerId, http.MethodPatch, "/v1/media/5", `{"filename":"spring"}`), http.StatusOK)
|
||||
|
||||
listing := p.as(t, apiKeyOwnerId, http.MethodGet, "/v1/activity?outcome=failed", "")
|
||||
assertAPIKeyStatus(t, listing, http.StatusOK)
|
||||
var page struct {
|
||||
Items []struct {
|
||||
Action string `json:"action"`
|
||||
FailureReason *string `json:"failure_reason"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(listing.Body.Bytes(), &page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(page.Items) != 1 || page.Items[0].Action != "media.merge" || page.Items[0].FailureReason == nil || *page.Items[0].FailureReason != "One of the copies no longer exists." {
|
||||
t.Errorf("items = %+v", page.Items)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/purge"
|
||||
"RenewCMS/internal/domain/update"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const producerSignalTimeout = 5 * time.Second
|
||||
|
||||
type producerSignalRecorder struct {
|
||||
inner audit.Recorder
|
||||
recorded chan struct{}
|
||||
}
|
||||
|
||||
var _ audit.Recorder = &producerSignalRecorder{}
|
||||
|
||||
func newProducerSignalRecorder(inner audit.Recorder) *producerSignalRecorder {
|
||||
return &producerSignalRecorder{inner: inner, recorded: make(chan struct{}, 16)}
|
||||
}
|
||||
|
||||
func (r *producerSignalRecorder) Record(ctx context.Context, event audit.Event) {
|
||||
r.inner.Record(ctx, event)
|
||||
r.recorded <- struct{}{}
|
||||
}
|
||||
|
||||
func (r *producerSignalRecorder) await(t *testing.T) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-r.recorded:
|
||||
case <-time.After(producerSignalTimeout):
|
||||
t.Fatal("no event was recorded")
|
||||
}
|
||||
}
|
||||
|
||||
type producerPurgeRunner struct {
|
||||
result purge.RunResult
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
var _ useCases.PurgeRunner = &producerPurgeRunner{}
|
||||
|
||||
func (r *producerPurgeRunner) Execute(*string) (purge.RunResult, error) {
|
||||
r.calls++
|
||||
return r.result, r.err
|
||||
}
|
||||
|
||||
type producerBlockingImageRepo struct {
|
||||
*mockImageRepo
|
||||
entered chan struct{}
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (r *producerBlockingImageRepo) ListDuplicates() ([]image.Image, error) {
|
||||
r.entered <- struct{}{}
|
||||
<-r.release
|
||||
return r.mockImageRepo.ListDuplicates()
|
||||
}
|
||||
|
||||
func producerContext(actor audit.Actor, source audit.Source) context.Context {
|
||||
return audit.WithOrigin(context.Background(), audit.Origin{Actor: actor, Source: source, IP: "192.0.2.1", UserAgent: "producers-test/1.0"})
|
||||
}
|
||||
|
||||
func producerOwnerContext() context.Context {
|
||||
id := uint32(1)
|
||||
return producerContext(audit.Actor{Type: audit.ActorUser, ID: &id, Name: "Marta Alves"}, audit.SourceWeb)
|
||||
}
|
||||
|
||||
func producerAnonymousContext() context.Context {
|
||||
return producerContext(audit.AnonymousActor(""), audit.SourceWeb)
|
||||
}
|
||||
|
||||
func onlyEvent(t *testing.T, h *activityHarness) audit.Event {
|
||||
t.Helper()
|
||||
events := h.all(t)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected exactly one event, got %d: %+v", len(events), events)
|
||||
}
|
||||
return events[0]
|
||||
}
|
||||
|
||||
func assertNoEvents(t *testing.T, h *activityHarness) {
|
||||
t.Helper()
|
||||
if events := h.all(t); len(events) != 0 {
|
||||
t.Fatalf("expected no event, got %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEventsHoldNothing(t *testing.T, h *activityHarness, forbidden ...string) {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(h.all(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, secret := range forbidden {
|
||||
if strings.Contains(string(encoded), secret) || strings.Contains(h.logs.String(), secret) {
|
||||
t.Errorf("an event or the log holds %q: %s", secret, encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityLogProducersArticleChanges(t *testing.T) {
|
||||
t.Run("The delta is signed and counts characters", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
before int
|
||||
after int
|
||||
delta int
|
||||
}{{100, 412, 312}, {412, 100, -312}, {5, 5, 0}} {
|
||||
t.Run(fmt.Sprintf("%d to %d", example.before, example.after), func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
repo := newMockArticleRepo()
|
||||
repo.articles[8] = article.Article{ID: 8, Title: "Spring release", Body: strings.Repeat("a", example.before)}
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, h.recorder)
|
||||
|
||||
if _, err := uc.UpdateBody(producerOwnerContext(), 8, strings.Repeat("b", example.after), article.BodyFormatHTML); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
if len(event.Changes) != 1 || event.Changes[0].Delta == nil || *event.Changes[0].Delta != example.delta || event.Changes[0].Field != "body" || event.Changes[0].Kind != audit.ChangeBody {
|
||||
t.Errorf("changes = %+v", event.Changes)
|
||||
}
|
||||
assertEventsHoldNothing(t, h, "bbbb")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A body with multi-byte characters is counted in characters", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
repo := newMockArticleRepo()
|
||||
repo.articles[8] = article.Article{ID: 8, Title: "Spring release", Body: "héllo"}
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, h.recorder)
|
||||
|
||||
if _, err := uc.UpdateBody(producerOwnerContext(), 8, "héllo wörld", article.BodyFormatHTML); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
if event.Changes[0].Delta == nil || *event.Changes[0].Delta != 6 || utf8.RuneCountInString("héllo wörld")-utf8.RuneCountInString("héllo") != 6 {
|
||||
t.Errorf("changes = %+v", event.Changes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A title change is recorded as a text diff when a title edit exists", func(t *testing.T) {
|
||||
before := article.Article{ID: 8, Title: "Spring release", Body: "x", IsOnline: false}
|
||||
after := article.Article{ID: 8, Title: "Spring release 2", Body: "x", IsOnline: false}
|
||||
|
||||
changes := audit.ArticleChanges(before, after)
|
||||
|
||||
encoded, _ := json.Marshal(changes)
|
||||
if string(encoded) != `[{"field":"title","kind":"text","before":"Spring release","after":"Spring release 2"}]` {
|
||||
t.Errorf("changes = %s", encoded)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Only the fields title, online and body can ever appear in an article event's changes", func(t *testing.T) {
|
||||
before := article.Article{ID: 8, Title: "a", Body: "x", IsOnline: false}
|
||||
after := article.Article{ID: 9, Title: "b", Body: "y", IsOnline: true}
|
||||
|
||||
for _, change := range audit.ArticleChanges(before, after) {
|
||||
if change.Field != "title" && change.Field != "online" && change.Field != "body" {
|
||||
t.Errorf("unexpected field %q", change.Field)
|
||||
}
|
||||
if change.Kind != audit.ChangeText && change.Kind != audit.ChangeBody {
|
||||
t.Errorf("unexpected kind %q", change.Kind)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A refused article title is recorded as failed with a plain reason", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
uc := useCases.NewCreateArticleUseCase(newMockArticleRepo(), &fakeBodyRenderer{}, h.recorder)
|
||||
|
||||
_, err := uc.CreateArticle(producerOwnerContext(), useCases.CreateArticleCommand{Title: "ab", Body: "Hello"})
|
||||
|
||||
if !errors.Is(err, article.ErrInvalidTitle) {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "article.create", audit.CategoryArticles, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, "The title must be between 3 and 200 characters.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersFailureReasons(t *testing.T) {
|
||||
sentences := []struct {
|
||||
cause error
|
||||
sentence string
|
||||
}{
|
||||
{article.ErrInvalidTitle, "The title must be between 3 and 200 characters."},
|
||||
{image.ErrImageTypeNotAllowed, "The file must be a PNG, JPEG, WEBP or SVG image."},
|
||||
{image.ErrImageTooLarge, "The file is too large to upload."},
|
||||
{image.ErrImageUnreadable, "The image could not be read."},
|
||||
{image.ErrInvalidFilename, "That file name is not valid."},
|
||||
{image.ErrInvalidMergeRequest, "The merge request is not valid."},
|
||||
{image.ErrMergeCopyMissing, "One of the copies no longer exists."},
|
||||
{image.ErrMergeHashMismatch, "The images are not identical copies."},
|
||||
{purge.ErrPurgeConflict, "The library changed since the preview. Review it again."},
|
||||
{user.ErrRegistrationClosed, "Registration is closed on this site."},
|
||||
{user.ErrVerificationEmailNotSent, "The verification email could not be sent."},
|
||||
{user.ErrInvalidVerificationCode, "The verification code is not valid."},
|
||||
{user.ErrVerificationCodeExpired, "The verification code has expired."},
|
||||
{user.ErrInvalidDisplayName, "That display name is too long."},
|
||||
{user.ErrUsernameTaken, "That username is already taken."},
|
||||
{invite.ErrEmailHasAccount, "That email already has an account."},
|
||||
{invite.ErrPendingInvite, "An invitation is already pending for that email."},
|
||||
{invite.ErrInviteEmailNotSent, "The invitation email could not be sent."},
|
||||
{invite.ErrInviteAlreadyAccepted, "That invitation was already accepted."},
|
||||
{invite.ErrInviteInvalid, "That invitation is not valid."},
|
||||
{invite.ErrInviteExpired, "That invitation has expired."},
|
||||
{update.ErrFeedUnavailable, "The release feed could not be reached."},
|
||||
{update.ErrNoRelease, "No release was found."},
|
||||
{update.ErrNotPrepared, "This server cannot apply updates."},
|
||||
{update.ErrJobInProgress, "An update is already being downloaded."},
|
||||
{update.ErrRestartPending, "A restart is already pending."},
|
||||
{update.ErrNothingToUpdate, "There is nothing to update."},
|
||||
{update.ErrPreflightRefused, "The update cannot be downloaded."},
|
||||
{update.ErrRestartKindMismatch, "The requested restart does not match the server state."},
|
||||
}
|
||||
|
||||
dueSettings := func() *mockPurgeSettingsRepo {
|
||||
repo := newMockPurgeSettingsRepo()
|
||||
repo.settings = purge.Settings{AutoPurgeEnabled: true, AutoPurgeIntervalDays: 1, EnabledAt: purgeTime("2026-09-01T00:00:00Z")}
|
||||
return repo
|
||||
}
|
||||
failWith := func(t *testing.T, err error) audit.Event {
|
||||
t.Helper()
|
||||
h := newActivityHarness(t)
|
||||
scheduler := useCases.NewPurgeSchedulerUseCase(dueSettings(), &producerPurgeRunner{err: err}, h.recorder)
|
||||
if _, _, tickErr := scheduler.Tick(*purgeTime("2026-10-05T09:00:00Z")); !errors.Is(tickErr, err) {
|
||||
t.Fatalf("tick err = %v", tickErr)
|
||||
}
|
||||
return onlyEvent(t, h)
|
||||
}
|
||||
|
||||
t.Run("Failure reasons are mapped from domain sentinels", func(t *testing.T) {
|
||||
for _, example := range sentences {
|
||||
t.Run(example.cause.Error(), func(t *testing.T) {
|
||||
event := failWith(t, fmt.Errorf("while working: %w", example.cause))
|
||||
assertProducerFailure(t, event, example.sentence)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unmapped errors become the generic sentence", func(t *testing.T) {
|
||||
for name, err := range map[string]error{
|
||||
"a plain error": errors.New("db is locked"),
|
||||
"an error carrying an SMTP host": errors.New("dial smtp.internal.example:587 with password hunter2"),
|
||||
"an error carrying a file system path": errors.New(`open C:\data\secret\file.db`),
|
||||
"an error wrapping a sentinel and a raw cause": errors.Join(update.ErrFeedUnavailable, errors.New("token=abc123")),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
event := failWith(t, err)
|
||||
if name == "an error wrapping a sentinel and a raw cause" {
|
||||
assertProducerFailure(t, event, "The release feed could not be reached.")
|
||||
} else {
|
||||
assertProducerFailure(t, event, useCases.GenericFailureReason)
|
||||
}
|
||||
encoded, _ := json.Marshal(event)
|
||||
for _, leaked := range []string{"locked", "hunter2", "smtp.internal", "secret", "abc123"} {
|
||||
if strings.Contains(string(encoded), leaked) {
|
||||
t.Errorf("the event leaks %q: %s", leaked, encoded)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Every failure reason fits the stored limit", func(t *testing.T) {
|
||||
for _, example := range sentences {
|
||||
if len(example.sentence) > 255 {
|
||||
t.Errorf("%q is %d characters", example.sentence, len(example.sentence))
|
||||
}
|
||||
}
|
||||
if len(useCases.GenericFailureReason) > 255 {
|
||||
t.Errorf("the generic sentence is too long")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A failed event keeps its target when the target is known", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
images := newMockImageRepo()
|
||||
images.images[5] = image.Image{ID: 5, Filename: "banner.png", Path: "/static/uploadedImages/5.png"}
|
||||
uc := useCases.NewRenameMediaUseCase(images, h.recorder)
|
||||
|
||||
if _, err := uc.Rename(producerOwnerContext(), 5, "not valid!"); !errors.Is(err, image.ErrInvalidFilename) {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
|
||||
assertProducerTarget(t, onlyEvent(t, h), "image", "5", "banner.png")
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersScheduledPurge(t *testing.T) {
|
||||
due := func() *mockPurgeSettingsRepo {
|
||||
repo := newMockPurgeSettingsRepo()
|
||||
repo.settings = purge.Settings{AutoPurgeEnabled: true, AutoPurgeIntervalDays: 10, EnabledAt: purgeTime("2026-09-20T09:00:00Z")}
|
||||
return repo
|
||||
}
|
||||
now := *purgeTime("2026-10-05T09:00:00Z")
|
||||
|
||||
t.Run("The scheduled purge is a system event", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
runner := &producerPurgeRunner{result: purge.RunResult{GroupsTotal: 2, GroupsPurged: 2, Merged: 3, ArticlesUpdated: 5}}
|
||||
scheduler := useCases.NewPurgeSchedulerUseCase(due(), runner, h.recorder)
|
||||
|
||||
if _, ran, err := scheduler.Tick(now); err != nil || !ran {
|
||||
t.Fatalf("ran = %v, err = %v", ran, err)
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.media_purge", audit.CategorySystem, audit.OutcomeSuccess)
|
||||
if event.Actor.Type != audit.ActorSystem || event.Actor.Name != "System" || event.Source != audit.SourceSystem || event.IP != nil || event.UserAgent != nil {
|
||||
t.Errorf("event = %+v", event)
|
||||
}
|
||||
if got := paramsJSON(t, event); got != `{"articles_updated":5,"merged":3}` {
|
||||
t.Errorf("params = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A scheduled purge with a failed group records the failed group count", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
runner := &producerPurgeRunner{result: purge.RunResult{GroupsTotal: 3, GroupsPurged: 2, FailedGroups: 1, Merged: 2, ArticlesUpdated: 2}}
|
||||
scheduler := useCases.NewPurgeSchedulerUseCase(due(), runner, h.recorder)
|
||||
|
||||
if _, _, err := scheduler.Tick(now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.media_purge", audit.CategorySystem, audit.OutcomeSuccess)
|
||||
if !strings.Contains(paramsJSON(t, event), `"failed_groups":1`) {
|
||||
t.Errorf("params = %s", paramsJSON(t, event))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A scheduled purge that is not due or merges nothing records nothing", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
notDue := newMockPurgeSettingsRepo()
|
||||
runner := &producerPurgeRunner{}
|
||||
if _, ran, err := useCases.NewPurgeSchedulerUseCase(notDue, runner, h.recorder).Tick(now); err != nil || ran {
|
||||
t.Fatalf("ran = %v, err = %v", ran, err)
|
||||
}
|
||||
if _, ran, err := useCases.NewPurgeSchedulerUseCase(due(), runner, h.recorder).Tick(now); err != nil || !ran {
|
||||
t.Fatalf("ran = %v, err = %v", ran, err)
|
||||
}
|
||||
assertNoEvents(t, h)
|
||||
})
|
||||
|
||||
t.Run("A scheduled purge that cannot record its run is a failed system event", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
runner := &producerPurgeRunner{err: errRepositoryFailure}
|
||||
|
||||
if _, _, err := useCases.NewPurgeSchedulerUseCase(due(), runner, h.recorder).Tick(now); err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.media_purge", audit.CategorySystem, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, useCases.GenericFailureReason)
|
||||
})
|
||||
|
||||
t.Run("A refused manual purge is recorded as failed when a purge is already running", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
images := &producerBlockingImageRepo{mockImageRepo: newMockImageRepo(), entered: make(chan struct{}, 2), release: make(chan struct{})}
|
||||
articles := newMockArticleRepo()
|
||||
storage := newMockImageStorage()
|
||||
merge := useCases.NewMergeMediaUseCase(images, articles, storage, h.recorder)
|
||||
run := useCases.NewRunPurgeUseCase(images, articles, merge, newMockPurgeSettingsRepo(), func() time.Time { return now }, log.New(io.Discard, "", 0), h.recorder)
|
||||
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
defer close(finished)
|
||||
_, _ = run.Run(producerOwnerContext(), nil)
|
||||
}()
|
||||
<-images.entered
|
||||
|
||||
_, err := run.Run(producerOwnerContext(), nil)
|
||||
close(images.release)
|
||||
<-finished
|
||||
|
||||
if !errors.Is(err, purge.ErrPurgeBusy) {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
events := h.all(t)
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("events = %+v", events)
|
||||
}
|
||||
busy := events[1]
|
||||
if events[0].Outcome == audit.OutcomeFailed {
|
||||
busy = events[0]
|
||||
}
|
||||
assertProducerEvent(t, busy, "media.purge", audit.CategoryMedia, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, busy, "A purge is already running.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersAccounts(t *testing.T) {
|
||||
const password = "s3cretpassw0rd"
|
||||
newUsers := func() *mockUserRepo {
|
||||
users := newMockUserRepo()
|
||||
return users
|
||||
}
|
||||
|
||||
t.Run("Registering is recorded with the new account as actor", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
uc := useCases.NewCreateUserUseCase(newUsers(), &stubHasher{}, newMockMailRepo(), "https://cms.example.com", h.recorder)
|
||||
|
||||
created, err := uc.CreateUser(producerAnonymousContext(), useCases.CreateUserCommand{Username: "marta", Password: password, Email: "marta@example.com"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "user.register", audit.CategoryUsers, audit.OutcomeSuccess)
|
||||
if event.Actor.Type != audit.ActorUser || event.Actor.ID == nil || *event.Actor.ID != created.ID || event.Actor.Name != "marta" || event.Source != audit.SourceWeb {
|
||||
t.Errorf("actor = %+v, source = %s", event.Actor, event.Source)
|
||||
}
|
||||
assertProducerTarget(t, event, "user", "1", "marta")
|
||||
assertEventsHoldNothing(t, h, password, "hashed:", created.VerificationCode)
|
||||
})
|
||||
|
||||
t.Run("A refused registration is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
cause string
|
||||
setup func(users *mockUserRepo, mail *mockMailRepo)
|
||||
reason string
|
||||
}{
|
||||
{"registration is closed", func(users *mockUserRepo, _ *mockMailRepo) {
|
||||
users.users[9] = user.User{ID: 9, Username: "owner", IsVerified: true}
|
||||
}, "Registration is closed on this site."},
|
||||
{"the verification email cannot be sent", func(_ *mockUserRepo, mail *mockMailRepo) { mail.fail.failNext("Send") }, "The verification email could not be sent."},
|
||||
} {
|
||||
t.Run(example.cause, func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
users := newUsers()
|
||||
mail := newMockMailRepo()
|
||||
example.setup(users, mail)
|
||||
uc := useCases.NewCreateUserUseCase(users, &stubHasher{}, mail, "https://cms.example.com", h.recorder)
|
||||
|
||||
if _, err := uc.CreateUser(producerAnonymousContext(), useCases.CreateUserCommand{Username: "marta", Password: password, Email: "marta@example.com"}); err == nil {
|
||||
t.Fatal("expected a refusal")
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "user.register", audit.CategoryUsers, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
if event.Actor.Type != audit.ActorAnonymous || event.Actor.Name != "marta" || event.Target != (audit.Target{}) {
|
||||
t.Errorf("actor = %+v, target = %+v", event.Actor, event.Target)
|
||||
}
|
||||
assertEventsHoldNothing(t, h, password)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A failed cleanup after an unsent verification email keeps both causes", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
users := newUsers()
|
||||
mail := newMockMailRepo()
|
||||
mail.fail.failNext("Send")
|
||||
cleanupFailure := errors.New("cleanup failure")
|
||||
users.fail.failNextWith("Delete", cleanupFailure)
|
||||
uc := useCases.NewCreateUserUseCase(users, &stubHasher{}, mail, "https://cms.example.com", h.recorder)
|
||||
|
||||
_, err := uc.CreateUser(producerAnonymousContext(), useCases.CreateUserCommand{Username: "marta", Password: password, Email: "marta@example.com"})
|
||||
|
||||
if !errors.Is(err, user.ErrVerificationEmailNotSent) || !errors.Is(err, cleanupFailure) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
assertProducerFailure(t, onlyEvent(t, h), "The verification email could not be sent.")
|
||||
})
|
||||
|
||||
t.Run("Verifying an account is recorded", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
users := newUsers()
|
||||
users.users[4] = user.User{ID: 4, Username: "lea", VerificationCode: "hashed:the-code", VerificationExpiration: time.Now().Add(time.Hour)}
|
||||
uc := useCases.NewVerifyUserUseCase(users, &stubHasher{}, h.recorder)
|
||||
|
||||
if _, err := uc.Verify(producerAnonymousContext(), 4, "the-code"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "user.verify", audit.CategoryUsers, audit.OutcomeSuccess)
|
||||
if event.Actor.Type != audit.ActorUser || event.Actor.ID == nil || *event.Actor.ID != 4 {
|
||||
t.Errorf("actor = %+v", event.Actor)
|
||||
}
|
||||
assertProducerTarget(t, event, "user", "4", "lea")
|
||||
assertEventsHoldNothing(t, h, "the-code", "hashed:")
|
||||
})
|
||||
|
||||
t.Run("A refused verification is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
code string
|
||||
expiration time.Time
|
||||
reason string
|
||||
}{
|
||||
{"ErrInvalidVerificationCode", "wrong", time.Now().Add(time.Hour), "The verification code is not valid."},
|
||||
{"ErrVerificationCodeExpired", "the-code", time.Now().Add(-time.Hour), "The verification code has expired."},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
users := newUsers()
|
||||
users.users[4] = user.User{ID: 4, Username: "lea", VerificationCode: "hashed:the-code", VerificationExpiration: example.expiration}
|
||||
uc := useCases.NewVerifyUserUseCase(users, &stubHasher{}, h.recorder)
|
||||
|
||||
if _, err := uc.Verify(producerAnonymousContext(), 4, example.code); err == nil {
|
||||
t.Fatal("expected a refusal")
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "user.verify", audit.CategoryUsers, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Accepting an invite is recorded with the new account as actor", func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
users := newUsers()
|
||||
invites := newInviteFakeRepo()
|
||||
invites.users = users
|
||||
seeded := invites.seed(invite.Invite{Email: "new@example.com", HashedCode: "hashed:the-code", ExpiresAt: time.Now().Add(time.Hour)})
|
||||
uc := useCases.NewAcceptInviteUseCase(users, invites, &stubHasher{}, h.recorder)
|
||||
|
||||
created, err := uc.AcceptInvite(producerAnonymousContext(), useCases.AcceptInviteCommand{Code: "the-code", Username: "sam", Password: password})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "invite.accept", audit.CategoryUsers, audit.OutcomeSuccess)
|
||||
if event.Actor.Type != audit.ActorUser || event.Actor.ID == nil || *event.Actor.ID != created.ID || event.Actor.Name != "sam" || event.Source != audit.SourceWeb {
|
||||
t.Errorf("actor = %+v", event.Actor)
|
||||
}
|
||||
assertProducerTarget(t, event, "invite", fmt.Sprint(seeded.ID), "new@example.com")
|
||||
assertEventsHoldNothing(t, h, "the-code", "hashed:", password)
|
||||
})
|
||||
|
||||
t.Run("A refused acceptance is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
code string
|
||||
expiresIn time.Duration
|
||||
existing []user.User
|
||||
reason string
|
||||
target bool
|
||||
}{
|
||||
{"ErrInviteInvalid", "wrong", time.Hour, nil, "That invitation is not valid.", false},
|
||||
{"ErrInviteExpired", "the-code", -time.Hour, nil, "That invitation has expired.", false},
|
||||
{"ErrUsernameTaken", "the-code", time.Hour, []user.User{{ID: 9, Username: "sam", Email: "sam@example.com"}}, "That username is already taken.", true},
|
||||
{"ErrEmailHasAccount", "the-code", time.Hour, []user.User{{ID: 9, Username: "other", Email: "new@example.com"}}, "That email already has an account.", true},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
h := newActivityHarness(t)
|
||||
users := newUsers()
|
||||
for _, existing := range example.existing {
|
||||
users.users[existing.ID] = existing
|
||||
}
|
||||
invites := newInviteFakeRepo()
|
||||
invites.users = users
|
||||
invites.seed(invite.Invite{Email: "new@example.com", HashedCode: "hashed:the-code", ExpiresAt: time.Now().Add(example.expiresIn)})
|
||||
uc := useCases.NewAcceptInviteUseCase(users, invites, &stubHasher{}, h.recorder)
|
||||
|
||||
if _, err := uc.AcceptInvite(producerAnonymousContext(), useCases.AcceptInviteCommand{Code: example.code, Username: "sam", Password: password}); err == nil {
|
||||
t.Fatal("expected a refusal")
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "invite.accept", audit.CategoryUsers, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
if event.Actor.Type != audit.ActorAnonymous || event.Actor.Name != "sam" {
|
||||
t.Errorf("actor = %+v", event.Actor)
|
||||
}
|
||||
if (event.Target != audit.Target{}) != example.target {
|
||||
t.Errorf("target = %+v", event.Target)
|
||||
}
|
||||
assertEventsHoldNothing(t, h, "the-code", password)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersUpdateDownload(t *testing.T) {
|
||||
cancelledOwnerContext := func() (context.Context, context.CancelFunc) {
|
||||
return context.WithCancel(producerOwnerContext())
|
||||
}
|
||||
newRig := func(t *testing.T) (*downloadRig, *activityHarness, *producerSignalRecorder) {
|
||||
t.Helper()
|
||||
h := newActivityHarness(t)
|
||||
signal := newProducerSignalRecorder(h.recorder)
|
||||
rig := newDownloadRig(t)
|
||||
rig.recorder = signal
|
||||
rig.rebuild()
|
||||
return rig, h, signal
|
||||
}
|
||||
|
||||
t.Run("A download that completes is recorded when the job is ready", func(t *testing.T) {
|
||||
rig, h, signal := newRig(t)
|
||||
ctx, cancel := cancelledOwnerContext()
|
||||
|
||||
if _, err := rig.start.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cancel()
|
||||
signal.await(t)
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.update_downloaded", audit.CategorySystem, audit.OutcomeSuccess)
|
||||
assertProducerTarget(t, event, "release", downloadTag, downloadTag)
|
||||
if event.Actor.Type != audit.ActorUser || event.Actor.Name != "Marta Alves" || event.Source != audit.SourceWeb || event.IP == nil || *event.IP != "192.0.2.1" {
|
||||
t.Errorf("event = %+v", event)
|
||||
}
|
||||
assertEventsHoldNothing(t, h, "https://", rig.updatesDir)
|
||||
})
|
||||
|
||||
t.Run("A download that fails is recorded with a reason that hides the cause", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
kind string
|
||||
setup func(rig *downloadRig)
|
||||
reason string
|
||||
}{
|
||||
{"download", func(rig *downloadRig) { rig.fetcher.errors[rig.assetName] = errDownloadNetwork }, "The update could not be downloaded."},
|
||||
{"verification", func(rig *downloadRig) {
|
||||
_, otherKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rig.signChecksums(rig.checksumsFor(rig.assetName, rig.fetcher.files[rig.assetName]), otherKey)
|
||||
rig.publishAssets()
|
||||
}, "The downloaded update failed its integrity check."},
|
||||
} {
|
||||
t.Run(example.kind, func(t *testing.T) {
|
||||
rig, h, signal := newRig(t)
|
||||
example.setup(rig)
|
||||
|
||||
if _, err := rig.start.Start(producerOwnerContext()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signal.await(t)
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.update_downloaded", audit.CategorySystem, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
assertEventsHoldNothing(t, h, "https://", rig.updatesDir, errDownloadNetwork.Error())
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A download refused at the start is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
setup func(rig *downloadRig)
|
||||
reason string
|
||||
}{
|
||||
{"ErrNotPrepared", func(rig *downloadRig) { rig.helper.installed = false }, "This server cannot apply updates."},
|
||||
{"ErrJobInProgress", func(rig *downloadRig) {
|
||||
rig.jobs.put(update.Job{Tag: downloadTag, State: update.StateDownloading, StartedAt: time.Now()})
|
||||
}, "An update is already being downloaded."},
|
||||
{"ErrRestartPending", func(rig *downloadRig) {
|
||||
_ = rig.requests.Write(update.RestartRequest{Kind: update.RestartKindRestart, RequestedAt: updateTestCheckedAt})
|
||||
}, "A restart is already pending."},
|
||||
{"ErrNothingToUpdate", func(rig *downloadRig) { rig.store.found = false }, "There is nothing to update."},
|
||||
{"ErrPreflightRefused", func(rig *downloadRig) { rig.dropAsset(update.ChecksumsAssetName) }, "The update cannot be downloaded."},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
rig, h, _ := newRig(t)
|
||||
example.setup(rig)
|
||||
|
||||
if _, err := rig.start.Start(producerOwnerContext()); err == nil {
|
||||
t.Fatal("expected a refusal")
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.update_downloaded", audit.CategorySystem, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityLogProducersRestart(t *testing.T) {
|
||||
newRequest := func(t *testing.T) (*restartRig, *activityHarness, *useCases.RequestRestartUseCase) {
|
||||
t.Helper()
|
||||
h := newActivityHarness(t)
|
||||
rig := newRestartRig(updateRunning)
|
||||
request := useCases.NewRequestRestartUseCase(rig.store, rig.jobs, rig.helper, rig.results, rig.requests, updateRunning, rig.clock.Now, h.recorder)
|
||||
return rig, h, request
|
||||
}
|
||||
|
||||
t.Run("A restart request is recorded", func(t *testing.T) {
|
||||
_, h, request := newRequest(t)
|
||||
|
||||
if _, err := request.Request(producerOwnerContext(), updateOwnerId, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.restart_requested", audit.CategorySystem, audit.OutcomeSuccess)
|
||||
if got := paramsJSON(t, event); got != `{"kind":"restart"}` {
|
||||
t.Errorf("params = %s", got)
|
||||
}
|
||||
if event.Target != (audit.Target{}) {
|
||||
t.Errorf("target = %+v", event.Target)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A restart that applies a downloaded update records the kind and version", func(t *testing.T) {
|
||||
rig, h, request := newRequest(t)
|
||||
rig.readyJob()
|
||||
|
||||
if _, err := request.Request(producerOwnerContext(), updateOwnerId, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := paramsJSON(t, onlyEvent(t, h)); got != `{"kind":"update","tag":"`+downloadTag+`"}` {
|
||||
t.Errorf("params = %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A refused restart is recorded as failed", func(t *testing.T) {
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
setup func(rig *restartRig)
|
||||
kind string
|
||||
reason string
|
||||
}{
|
||||
{"ErrNotPrepared", func(rig *restartRig) { rig.helper.installed = false }, "", "This server cannot apply updates."},
|
||||
{"ErrJobInProgress", func(rig *restartRig) {
|
||||
rig.jobs.put(update.Job{Tag: downloadTag, State: update.StateDownloading, StartedAt: restartBaseTime})
|
||||
}, "", "An update is still being downloaded."},
|
||||
{"ErrRestartPending", func(rig *restartRig) {
|
||||
_ = rig.requests.Write(update.RestartRequest{Kind: update.RestartKindRestart, RequestedAt: restartBaseTime})
|
||||
}, "", "A restart is already pending."},
|
||||
{"ErrRestartKindMismatch", func(*restartRig) {}, update.RestartKindUpdate, "The requested restart does not match the server state."},
|
||||
} {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
rig, h, request := newRequest(t)
|
||||
example.setup(rig)
|
||||
|
||||
if _, err := request.Request(producerOwnerContext(), updateOwnerId, example.kind); err == nil {
|
||||
t.Fatal("expected a refusal")
|
||||
}
|
||||
|
||||
event := onlyEvent(t, h)
|
||||
assertProducerEvent(t, event, "system.restart_requested", audit.CategorySystem, audit.OutcomeFailed)
|
||||
assertProducerFailure(t, event, example.reason)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
+35
-20
@@ -6,6 +6,7 @@ import (
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
"RenewCMS/internal/domain/apikey"
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/update"
|
||||
"RenewCMS/internal/domain/user"
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
@@ -45,6 +48,7 @@ type apiKeyEnv struct {
|
||||
articles *mockArticleRepo
|
||||
images *mockImageRepo
|
||||
invites *inviteFakeRepo
|
||||
mail *mockMailRepo
|
||||
feed *fakeReleaseFeed
|
||||
updates *fakeUpdateStore
|
||||
token *jwtauth.JWTAuth
|
||||
@@ -57,6 +61,11 @@ type apiKeyEnv struct {
|
||||
}
|
||||
|
||||
func newAPIKeyEnv(t *testing.T) *apiKeyEnv {
|
||||
t.Helper()
|
||||
return newAPIKeyEnvRecording(t, discardRecorder)
|
||||
}
|
||||
|
||||
func newAPIKeyEnvRecording(t *testing.T, recorder audit.Recorder) *apiKeyEnv {
|
||||
t.Helper()
|
||||
validator.InitValidator()
|
||||
|
||||
@@ -89,41 +98,47 @@ func newAPIKeyEnv(t *testing.T) *apiKeyEnv {
|
||||
clock := func() time.Time { return env.now }
|
||||
hasher := &stubHasher{}
|
||||
mail := newMockMailRepo()
|
||||
env.mail = mail
|
||||
renderer := &fakeBodyRenderer{}
|
||||
mergeMedia := useCases.NewMergeMediaUseCase(env.images, env.articles, newMockImageStorage(), recorder)
|
||||
purgeSettings := newMockPurgeSettingsRepo()
|
||||
restartJobs := &fakeJobStore{}
|
||||
env.container = &api.UseCases{
|
||||
Token: env.token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(env.articles, renderer),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(env.articles, renderer, recorder),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(env.articles, renderer),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles, renderer),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles, renderer),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(env.articles),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles, renderer, recorder),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(env.articles, recorder),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(env.users),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(env.users, hasher, mail, "https://cms.example.com"),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(env.users, hasher, discardRecorder),
|
||||
VerifyUserUseCase: useCases.NewVerifyUserUseCase(env.users, hasher),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(env.users, hasher, mail, "https://cms.example.com", recorder),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(env.users, hasher, recorder),
|
||||
VerifyUserUseCase: useCases.NewVerifyUserUseCase(env.users, hasher, recorder),
|
||||
CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(env.users),
|
||||
RequestPasswordResetUseCase: useCases.NewRequestPasswordResetUseCase(env.users, hasher, mail, "https://cms.example.com", discardRecorder),
|
||||
ResetPasswordUseCase: useCases.NewResetPasswordUseCase(env.users, hasher, env.keys, clock, discardRecorder),
|
||||
UpdateUserProfileUseCase: useCases.NewUpdateUserProfileUseCase(env.users),
|
||||
CreateInviteUseCase: useCases.NewCreateInviteUseCase(env.users, env.invites, hasher, mail, "https://cms.example.com"),
|
||||
CancelInviteUseCase: useCases.NewCancelInviteUseCase(env.invites),
|
||||
RenewInviteUseCase: useCases.NewRenewInviteUseCase(env.users, env.invites, hasher, mail, "https://cms.example.com"),
|
||||
RequestPasswordResetUseCase: useCases.NewRequestPasswordResetUseCase(env.users, hasher, mail, "https://cms.example.com", recorder),
|
||||
ResetPasswordUseCase: useCases.NewResetPasswordUseCase(env.users, hasher, env.keys, clock, recorder),
|
||||
UpdateUserProfileUseCase: useCases.NewUpdateUserProfileUseCase(env.users, recorder),
|
||||
CreateInviteUseCase: useCases.NewCreateInviteUseCase(env.users, env.invites, hasher, mail, "https://cms.example.com", recorder),
|
||||
CancelInviteUseCase: useCases.NewCancelInviteUseCase(env.invites, recorder),
|
||||
RenewInviteUseCase: useCases.NewRenewInviteUseCase(env.users, env.invites, hasher, mail, "https://cms.example.com", recorder),
|
||||
ListUsersAndInvitesUseCase: useCases.NewListUsersAndInvitesUseCase(env.users, env.invites),
|
||||
GetInviteUseCase: useCases.NewGetInviteUseCase(env.invites, hasher),
|
||||
AcceptInviteUseCase: useCases.NewAcceptInviteUseCase(env.users, env.invites, hasher),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(env.images, newMockImageStorage()),
|
||||
AcceptInviteUseCase: useCases.NewAcceptInviteUseCase(env.users, env.invites, hasher, recorder),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(env.images, newMockImageStorage(), recorder),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(env.images, env.articles, env.users),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(env.images),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(env.images, newMockImageStorage()),
|
||||
MergeMediaUseCase: useCases.NewMergeMediaUseCase(env.images, env.articles, newMockImageStorage()),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(env.images, recorder),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(env.images, newMockImageStorage(), recorder),
|
||||
MergeMediaUseCase: mergeMedia,
|
||||
RunPurgeUseCase: useCases.NewRunPurgeUseCase(env.images, env.articles, mergeMedia, purgeSettings, clock, log.New(io.Discard, "", 0), recorder),
|
||||
RequestRestartUseCase: useCases.NewRequestRestartUseCase(env.updates, restartJobs, &fakeHelperDetector{installed: true}, &fakeResultStore{}, &fakeRestartRequestStore{}, apiKeyRunningVersion, clock, recorder),
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(env.images, env.articles),
|
||||
GetSettingsUseCase: useCases.NewGetSettingsUseCase(apiKeyRunningVersion),
|
||||
GetActivityRetentionUseCase: useCases.NewGetActivityRetentionUseCase(newActivitySettingsFake()),
|
||||
GetUpdateStatusUseCase: newUpdateStatusUseCase(env.updates, &fakeJobStore{}, &fakeHelperDetector{}, apiKeyRunningVersion),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(env.feed, env.updates, apiKeyRunningVersion, clock),
|
||||
CreateAPIKeyUseCase: useCases.NewCreateAPIKeyUseCase(env.keys, env.generator, clock, discardRecorder),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(env.feed, env.updates, apiKeyRunningVersion, clock, recorder),
|
||||
CreateAPIKeyUseCase: useCases.NewCreateAPIKeyUseCase(env.keys, env.generator, clock, recorder),
|
||||
ListAPIKeysUseCase: useCases.NewListAPIKeysUseCase(env.keys, env.users, clock),
|
||||
RevokeAPIKeyUseCase: useCases.NewRevokeAPIKeyUseCase(env.keys, env.users, clock, discardRecorder),
|
||||
RevokeAPIKeyUseCase: useCases.NewRevokeAPIKeyUseCase(env.keys, env.users, clock, recorder),
|
||||
AuthenticateAPIKeyUseCase: useCases.NewAuthenticateAPIKeyUseCase(env.keys, env.generator, env.users, clock),
|
||||
}
|
||||
router := chi.NewRouter()
|
||||
|
||||
@@ -43,11 +43,11 @@ const (
|
||||
func newBodyFormatRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, renderer domainArticle.BodyRenderer, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, renderer),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, renderer, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, renderer),
|
||||
useCases.NewListArticlesUseCase(articleRepo, renderer),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, renderer),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, renderer, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ func newArticlePublishingRouter(articleRepo *mockArticleRepo, token *jwtauth.JWT
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -148,11 +148,11 @@ var _ article.Repository = &mockArticleRepo{}
|
||||
|
||||
func TestCreateArticle(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
|
||||
t.Run("Valid Title", func(t *testing.T) {
|
||||
cmd := useCases.CreateArticleCommand{Title: "Valid Title", Body: "Content"}
|
||||
res, err := uc.CreateArticle(cmd)
|
||||
res, err := uc.CreateArticle(testCtx, cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func TestCreateArticle(t *testing.T) {
|
||||
|
||||
t.Run("Short Title", func(t *testing.T) {
|
||||
cmd := useCases.CreateArticleCommand{Title: "Ab", Body: "Content"}
|
||||
_, err := uc.CreateArticle(cmd)
|
||||
_, err := uc.CreateArticle(testCtx, cmd)
|
||||
if !errors.Is(err, article.ErrInvalidTitle) {
|
||||
t.Errorf("Expected ErrInvalidTitle")
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ func newArticleTitleRulesRouter(repo *mockArticleRepo, token *jwtauth.JWTAuth) *
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(repo),
|
||||
useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(repo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
@@ -87,10 +87,10 @@ func TestArticleTitleRunesInUseCase(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("rune_count_%d", tc.runeCount), func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
|
||||
title := strings.Repeat("\U0001F600", tc.runeCount)
|
||||
result, err := uc.CreateArticle(useCases.CreateArticleCommand{Title: title, Body: "Some body text"})
|
||||
result, err := uc.CreateArticle(testCtx, useCases.CreateArticleCommand{Title: title, Body: "Some body text"})
|
||||
|
||||
if tc.created {
|
||||
if err != nil {
|
||||
|
||||
@@ -20,11 +20,11 @@ func newArticlesListRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth)
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ func newCancelRegistrationRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo,
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher),
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder),
|
||||
useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -52,17 +52,18 @@ func newContainerHarness(t *testing.T, container bool, helperInstalled bool) *co
|
||||
Platform: rig.platform,
|
||||
FeedURL: downloadFeedURL,
|
||||
Now: now,
|
||||
Recorder: discardRecorder,
|
||||
})
|
||||
routes := &api.UseCases{
|
||||
Token: token,
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(newUpdateUserRepo()),
|
||||
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(rig.store, rig.jobs, helper, rig.results, rig.requests, updateRunning, container, now),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, rig.store, updateRunning, now),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, rig.store, updateRunning, now, discardRecorder),
|
||||
StartUpdateDownloadUseCase: start,
|
||||
RequestRestartUseCase: useCases.NewRequestRestartUseCase(rig.store, rig.jobs, helper, rig.results, rig.requests, updateRunning, now),
|
||||
RequestRestartUseCase: useCases.NewRequestRestartUseCase(rig.store, rig.jobs, helper, rig.results, rig.requests, updateRunning, now, discardRecorder),
|
||||
}
|
||||
return &containerHarness{
|
||||
updateHarness: &updateHarness{router: api.NewSystemRouter(routes), token: token, feed: feed, store: rig.store},
|
||||
updateHarness: &updateHarness{router: api.NewSystemRouter(routes, nil), token: token, feed: feed, store: rig.store},
|
||||
fetcher: rig.fetcher,
|
||||
requests: rig.requests,
|
||||
jobs: rig.jobs,
|
||||
|
||||
@@ -24,11 +24,11 @@ func newEditArticleBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAu
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func newGetCurrentUserRouter(userRepo *mockUserRepo, hasher *stubHasher, token *
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
meHandler := auth.NewMeHandler(getUserUseCase)
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher),
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder),
|
||||
useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
true,
|
||||
)
|
||||
|
||||
+6
-6
@@ -34,9 +34,9 @@ func (s *spyHasher) Compare(hashed string, value string) error {
|
||||
func TestCreateUserUseCaseHashesPasswordAndVerificationCode(t *testing.T) {
|
||||
t.Run("stores hashed password and hashed verification code, returns raw code", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder)
|
||||
|
||||
result, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
result, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "alice",
|
||||
Password: "s3cretPassw0rd",
|
||||
Email: "alice@example.com",
|
||||
@@ -59,9 +59,9 @@ func TestCreateUserUseCaseHashesPasswordAndVerificationCode(t *testing.T) {
|
||||
|
||||
t.Run("a hashing failure persists nothing", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{hashErr: errStubHashFailure}, newMockMailRepo(), "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{hashErr: errStubHashFailure}, newMockMailRepo(), "https://cms.example.com", discardRecorder)
|
||||
|
||||
_, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
_, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "alice",
|
||||
Password: "s3cretPassw0rd",
|
||||
Email: "alice@example.com",
|
||||
@@ -156,7 +156,7 @@ func newHasherRouter(userRepo *mockUserRepo, hasher *stubHasher) *chi.Mux {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
@@ -174,7 +174,7 @@ func newRealHasherRouter(userRepo *mockUserRepo) *chi.Mux {
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
|
||||
+5
-5
@@ -249,8 +249,8 @@ var _ imageDomain.Repository = &mockImageRepo{}
|
||||
|
||||
func TestImageUseCases(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
deleteUc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage())
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
deleteUc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
t.Run("Create Image", func(t *testing.T) {
|
||||
input := imageDomain.ImageInput{
|
||||
@@ -259,7 +259,7 @@ func TestImageUseCases(t *testing.T) {
|
||||
ContentType: "image/png",
|
||||
}
|
||||
|
||||
res, err := createUc.CreateImage(input)
|
||||
res, err := createUc.CreateImage(testCtx, input)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateImage failed: %v", err)
|
||||
}
|
||||
@@ -274,7 +274,7 @@ func TestImageUseCases(t *testing.T) {
|
||||
t.Run("Delete Image", func(t *testing.T) {
|
||||
repo.images[99] = imageDomain.Image{ID: 99, Path: "/static/delete-me.png"}
|
||||
|
||||
err := deleteUc.Delete(99)
|
||||
err := deleteUc.Delete(testCtx, 99)
|
||||
if err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
@@ -285,7 +285,7 @@ func TestImageUseCases(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Delete unknown image", func(t *testing.T) {
|
||||
if err := deleteUc.Delete(12345); !errors.Is(err, imageDomain.ErrImageNotFound) {
|
||||
if err := deleteUc.Delete(testCtx, 12345); !errors.Is(err, imageDomain.ErrImageNotFound) {
|
||||
t.Errorf("expected ErrImageNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,9 +39,9 @@ func TestImageUploadRules(t *testing.T) {
|
||||
for _, contentType := range []string{"image/png", "image/jpeg", "image/webp", "image/svg+xml"} {
|
||||
t.Run(contentType, func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
result, err := createUc.CreateImage(inputOf(paddedTo(validBytesFor(t, contentType), int(imageDomain.MaxImageSize)), contentType))
|
||||
result, err := createUc.CreateImage(testCtx, inputOf(paddedTo(validBytesFor(t, contentType), int(imageDomain.MaxImageSize)), contentType))
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
@@ -58,9 +58,9 @@ func TestImageUploadRules(t *testing.T) {
|
||||
|
||||
t.Run("CreateImage rejects a disallowed content type before reaching the repository", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
_, err := createUc.CreateImage(inputOf([]byte("%PDF"), "application/pdf"))
|
||||
_, err := createUc.CreateImage(testCtx, inputOf([]byte("%PDF"), "application/pdf"))
|
||||
|
||||
if !errors.Is(err, imageDomain.ErrImageTypeNotAllowed) {
|
||||
t.Fatalf("expected ErrImageTypeNotAllowed, got %v", err)
|
||||
@@ -72,9 +72,9 @@ func TestImageUploadRules(t *testing.T) {
|
||||
|
||||
t.Run("CreateImage rejects a file that exceeds the size limit", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
_, err := createUc.CreateImage(inputOf(paddedTo(pngBytes(t, 3, 2), int(imageDomain.MaxImageSize)+1), "image/png"))
|
||||
_, err := createUc.CreateImage(testCtx, inputOf(paddedTo(pngBytes(t, 3, 2), int(imageDomain.MaxImageSize)+1), "image/png"))
|
||||
|
||||
if !errors.Is(err, imageDomain.ErrImageTooLarge) {
|
||||
t.Fatalf("expected ErrImageTooLarge, got %v", err)
|
||||
@@ -86,9 +86,9 @@ func TestImageUploadRules(t *testing.T) {
|
||||
|
||||
t.Run("A file exactly at the size limit is accepted, one byte over is not", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
_, err := createUc.CreateImage(inputOf(paddedTo(pngBytes(t, 3, 2), int(imageDomain.MaxImageSize)), "image/png"))
|
||||
_, err := createUc.CreateImage(testCtx, inputOf(paddedTo(pngBytes(t, 3, 2), int(imageDomain.MaxImageSize)), "image/png"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error at the exact limit, got %v", err)
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func TestImageUploadRules(t *testing.T) {
|
||||
t.Fatalf("expected Create to be called once, got %d calls", len(repo.images))
|
||||
}
|
||||
|
||||
_, err = createUc.CreateImage(inputOf(paddedTo(pngBytes(t, 3, 2), int(imageDomain.MaxImageSize)+1), "image/png"))
|
||||
_, err = createUc.CreateImage(testCtx, inputOf(paddedTo(pngBytes(t, 3, 2), int(imageDomain.MaxImageSize)+1), "image/png"))
|
||||
if !errors.Is(err, imageDomain.ErrImageTooLarge) {
|
||||
t.Fatalf("expected ErrImageTooLarge, got %v", err)
|
||||
}
|
||||
@@ -108,9 +108,9 @@ func TestImageUploadRules(t *testing.T) {
|
||||
t.Run("A repository failure unrelated to type or size still propagates unchanged", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
repo.fail.failNextWith("Create", errRepositoryFailure)
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
_, err := createUc.CreateImage(inputOf(pngBytes(t, 3, 2), "image/png"))
|
||||
_, err := createUc.CreateImage(testCtx, inputOf(pngBytes(t, 3, 2), "image/png"))
|
||||
|
||||
if !errors.Is(err, errRepositoryFailure) {
|
||||
t.Fatalf("expected the generic repository error, got %v", err)
|
||||
|
||||
+11
-11
@@ -152,7 +152,7 @@ func newInviteFixture(t *testing.T, maxAttempts int) *inviteFixture {
|
||||
}
|
||||
getUser := useCases.NewGetUserUseCase(f.users)
|
||||
handler := userhandler.NewInviteHandler(
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL),
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
nil,
|
||||
nil,
|
||||
useCases.NewListUsersAndInvitesUseCase(f.users, f.invites),
|
||||
@@ -536,7 +536,7 @@ func TestInvitationsDoNotChangeRegistration(t *testing.T) {
|
||||
hasher := &stubHasher{}
|
||||
handler := auth.NewLoginHandler(
|
||||
jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
useCases.NewCreateUserUseCase(users, hasher, mail, invitePublicURL),
|
||||
useCases.NewCreateUserUseCase(users, hasher, mail, invitePublicURL, discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(users, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
@@ -601,12 +601,12 @@ func TestInvitationsDoNotChangeRegistration(t *testing.T) {
|
||||
func TestCreateInviteUseCase(t *testing.T) {
|
||||
newUseCase := func() (*useCases.CreateInviteUseCase, *mockUserRepo, *inviteFakeRepo, *mockMailRepo, *stubHasher) {
|
||||
users, invites, mail, hasher := inviteInviterRepo("Marta A"), newInviteFakeRepo(), newMockMailRepo(), &stubHasher{}
|
||||
return useCases.NewCreateInviteUseCase(users, invites, hasher, mail, invitePublicURL), users, invites, mail, hasher
|
||||
return useCases.NewCreateInviteUseCase(users, invites, hasher, mail, invitePublicURL, discardRecorder), users, invites, mail, hasher
|
||||
}
|
||||
|
||||
t.Run("lower-cases the email and returns the stored invite", func(t *testing.T) {
|
||||
uc, _, _, _, _ := newUseCase()
|
||||
got, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "A@B.Example", InvitedBy: 7})
|
||||
got, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "A@B.Example", InvitedBy: 7})
|
||||
if err != nil || got.Email != "a@b.example" || got.ID == 0 {
|
||||
t.Fatalf("unexpected result %+v, %v", got, err)
|
||||
}
|
||||
@@ -614,7 +614,7 @@ func TestCreateInviteUseCase(t *testing.T) {
|
||||
|
||||
t.Run("unknown inviter", func(t *testing.T) {
|
||||
uc, _, invites, _, _ := newUseCase()
|
||||
_, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 99})
|
||||
_, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 99})
|
||||
if !errors.Is(err, user.ErrUserNotFound) || len(invites.invites) != 0 {
|
||||
t.Errorf("expected ErrUserNotFound, got %v", err)
|
||||
}
|
||||
@@ -623,7 +623,7 @@ func TestCreateInviteUseCase(t *testing.T) {
|
||||
t.Run("existing account is refused case-insensitively", func(t *testing.T) {
|
||||
uc, users, _, _, _ := newUseCase()
|
||||
users.users[8] = user.User{ID: 8, Email: "Jonas@Renew.example"}
|
||||
_, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "jonas@renew.example", InvitedBy: 7})
|
||||
_, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "jonas@renew.example", InvitedBy: 7})
|
||||
if !errors.Is(err, invite.ErrEmailHasAccount) {
|
||||
t.Errorf("expected ErrEmailHasAccount, got %v", err)
|
||||
}
|
||||
@@ -632,7 +632,7 @@ func TestCreateInviteUseCase(t *testing.T) {
|
||||
t.Run("pending invite is refused", func(t *testing.T) {
|
||||
uc, _, invites, _, _ := newUseCase()
|
||||
invites.seed(pendingInvite("a@b.example"))
|
||||
_, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
_, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
if !errors.Is(err, invite.ErrPendingInvite) {
|
||||
t.Errorf("expected ErrPendingInvite, got %v", err)
|
||||
}
|
||||
@@ -645,7 +645,7 @@ func TestCreateInviteUseCase(t *testing.T) {
|
||||
accepted.AcceptedAt = &acceptedAt
|
||||
invites.seed(accepted)
|
||||
invites.seed(pendingInvite("other@b.example"))
|
||||
if _, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7}); err != nil {
|
||||
if _, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7}); err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
if len(invites.invites) != 3 {
|
||||
@@ -656,7 +656,7 @@ func TestCreateInviteUseCase(t *testing.T) {
|
||||
t.Run("hash failure stores and sends nothing", func(t *testing.T) {
|
||||
uc, _, invites, mail, hasher := newUseCase()
|
||||
hasher.hashErr = errors.New("hash boom")
|
||||
_, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
_, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
if err == nil || len(invites.invites) != 0 || len(mail.SentEmails) != 0 {
|
||||
t.Errorf("expected failure without side effects, got %v", err)
|
||||
}
|
||||
@@ -666,7 +666,7 @@ func TestCreateInviteUseCase(t *testing.T) {
|
||||
uc, _, invites, mail, _ := newUseCase()
|
||||
invites.seed(expiredInvite("a@b.example"))
|
||||
invites.fail.failNext("Delete")
|
||||
_, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
_, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
if !errors.Is(err, errRepositoryFailure) || len(mail.SentEmails) != 0 {
|
||||
t.Errorf("expected repository failure, got %v", err)
|
||||
}
|
||||
@@ -676,7 +676,7 @@ func TestCreateInviteUseCase(t *testing.T) {
|
||||
uc, _, invites, mail, _ := newUseCase()
|
||||
mail.fail.failNextWith("Send", errors.New("smtp down"))
|
||||
invites.fail.failNextWith("Delete", errors.New("cleanup down"))
|
||||
_, err := uc.CreateInvite(useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
_, err := uc.CreateInvite(testCtx, useCases.CreateInviteCommand{Email: "a@b.example", InvitedBy: 7})
|
||||
if !errors.Is(err, invite.ErrInviteEmailNotSent) || !strings.Contains(err.Error(), "smtp down") || !strings.Contains(err.Error(), "cleanup down") {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ func newInviteManagementFixture(t *testing.T, maxAttempts int) *inviteManagement
|
||||
|
||||
getUser := useCases.NewGetUserUseCase(f.users)
|
||||
handler := userhandler.NewInviteHandler(
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL),
|
||||
useCases.NewCancelInviteUseCase(f.invites),
|
||||
useCases.NewRenewInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL),
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
useCases.NewCancelInviteUseCase(f.invites, discardRecorder),
|
||||
useCases.NewRenewInviteUseCase(f.users, f.invites, f.hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
useCases.NewListUsersAndInvitesUseCase(f.users, f.invites),
|
||||
)
|
||||
acceptHandler := auth.NewAcceptInviteHandler(
|
||||
useCases.NewGetInviteUseCase(f.invites, f.hasher),
|
||||
useCases.NewAcceptInviteUseCase(f.users, f.invites, f.hasher),
|
||||
useCases.NewAcceptInviteUseCase(f.users, f.invites, f.hasher, discardRecorder),
|
||||
f.token,
|
||||
true,
|
||||
)
|
||||
@@ -480,9 +480,9 @@ func TestManageInviteUseCases(t *testing.T) {
|
||||
mail := newMockMailRepo()
|
||||
seeded := invites.seed(pendingInvite("a@renew.example"))
|
||||
mail.fail.failNextWith("Send", errors.New("smtp down"))
|
||||
renew := useCases.NewRenewInviteUseCase(users, invites, &stubHasher{}, mail, invitePublicURL)
|
||||
renew := useCases.NewRenewInviteUseCase(users, invites, &stubHasher{}, mail, invitePublicURL, discardRecorder)
|
||||
|
||||
_, err := renew.RenewInvite(useCases.RenewInviteCommand{ID: 1, RenewedBy: 7})
|
||||
_, err := renew.RenewInvite(testCtx, useCases.RenewInviteCommand{ID: 1, RenewedBy: 7})
|
||||
|
||||
if !errors.Is(err, invite.ErrInviteEmailNotSent) || errors.Is(err, errRepositoryFailure) || invites.invites[1] != seeded {
|
||||
t.Errorf("unexpected result %v", err)
|
||||
@@ -495,9 +495,9 @@ func TestManageInviteUseCases(t *testing.T) {
|
||||
mail := newMockMailRepo()
|
||||
invites.seed(pendingInvite("a@renew.example"))
|
||||
mail.fail.failNextWith("Send", errors.New("smtp down"))
|
||||
renew := useCases.NewRenewInviteUseCase(users, &renewRestoreFailRepo{inviteFakeRepo: invites}, &stubHasher{}, mail, invitePublicURL)
|
||||
renew := useCases.NewRenewInviteUseCase(users, &renewRestoreFailRepo{inviteFakeRepo: invites}, &stubHasher{}, mail, invitePublicURL, discardRecorder)
|
||||
|
||||
_, err := renew.RenewInvite(useCases.RenewInviteCommand{ID: 1, RenewedBy: 7})
|
||||
_, err := renew.RenewInvite(testCtx, useCases.RenewInviteCommand{ID: 1, RenewedBy: 7})
|
||||
|
||||
if !errors.Is(err, invite.ErrInviteEmailNotSent) || !errors.Is(err, errRepositoryFailure) {
|
||||
t.Errorf("unexpected result %v", err)
|
||||
@@ -511,15 +511,15 @@ func TestManageInviteUseCases(t *testing.T) {
|
||||
at := time.Now()
|
||||
accepted.AcceptedAt = &at
|
||||
invites.seed(accepted)
|
||||
cancel := useCases.NewCancelInviteUseCase(invites)
|
||||
cancel := useCases.NewCancelInviteUseCase(invites, discardRecorder)
|
||||
|
||||
if err := cancel.CancelInvite(9); !errors.Is(err, invite.ErrInviteNotFound) {
|
||||
if err := cancel.CancelInvite(testCtx, 9); !errors.Is(err, invite.ErrInviteNotFound) {
|
||||
t.Errorf("expected not found, got %v", err)
|
||||
}
|
||||
if err := cancel.CancelInvite(2); !errors.Is(err, invite.ErrInviteAlreadyAccepted) {
|
||||
if err := cancel.CancelInvite(testCtx, 2); !errors.Is(err, invite.ErrInviteAlreadyAccepted) {
|
||||
t.Errorf("expected already accepted, got %v", err)
|
||||
}
|
||||
if err := cancel.CancelInvite(1); err != nil {
|
||||
if err := cancel.CancelInvite(testCtx, 1); err != nil {
|
||||
t.Errorf("unexpected %v", err)
|
||||
}
|
||||
})
|
||||
@@ -530,9 +530,9 @@ func TestManageInviteUseCases(t *testing.T) {
|
||||
mail := newMockMailRepo()
|
||||
seeded := invites.seed(pendingInvite("a@renew.example"))
|
||||
invites.fail.failNextWith("Renew", errRepositoryFailure)
|
||||
renew := useCases.NewRenewInviteUseCase(users, invites, &stubHasher{}, mail, invitePublicURL)
|
||||
renew := useCases.NewRenewInviteUseCase(users, invites, &stubHasher{}, mail, invitePublicURL, discardRecorder)
|
||||
|
||||
_, err := renew.RenewInvite(useCases.RenewInviteCommand{ID: 1, RenewedBy: 7})
|
||||
_, err := renew.RenewInvite(testCtx, useCases.RenewInviteCommand{ID: 1, RenewedBy: 7})
|
||||
|
||||
if !errors.Is(err, errRepositoryFailure) || len(mail.SentEmails) != 0 || invites.invites[1] != seeded {
|
||||
t.Errorf("unexpected result %v", err)
|
||||
@@ -544,9 +544,9 @@ func TestManageInviteUseCases(t *testing.T) {
|
||||
invites := newInviteFakeRepo()
|
||||
mail := newMockMailRepo()
|
||||
invites.seed(pendingInvite("a@renew.example"))
|
||||
renew := useCases.NewRenewInviteUseCase(users, invites, &stubHasher{}, mail, invitePublicURL)
|
||||
renew := useCases.NewRenewInviteUseCase(users, invites, &stubHasher{}, mail, invitePublicURL, discardRecorder)
|
||||
|
||||
if _, err := renew.RenewInvite(useCases.RenewInviteCommand{ID: 1, RenewedBy: 7}); err != nil {
|
||||
if _, err := renew.RenewInvite(testCtx, useCases.RenewInviteCommand{ID: 1, RenewedBy: 7}); err != nil {
|
||||
t.Fatalf("unexpected %v", err)
|
||||
}
|
||||
if inviteMailData(t, mail).InvitedBy != "Marta L." {
|
||||
|
||||
@@ -36,7 +36,7 @@ func newRateLimitedAuthRouter(t *testing.T, userRepo user.Repository, mailRepo *
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ func newLoginLogoutCookieRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo,
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -60,9 +60,9 @@ func TestMediaMergeRewritesArticlesInIdOrder(t *testing.T) {
|
||||
env.seedArticle(id, "Article", true, env.urlOf(3))
|
||||
}
|
||||
recorder := &mediaOrderRecordingArticleRepo{mockArticleRepo: env.articles}
|
||||
uc := useCases.NewMergeMediaUseCase(env.images, recorder, env.storage)
|
||||
uc := useCases.NewMergeMediaUseCase(env.images, recorder, env.storage, discardRecorder)
|
||||
|
||||
if _, err := uc.Merge(2, []uint32{3}); err != nil {
|
||||
if _, err := uc.Merge(testCtx, 2, []uint32{3}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,26 +60,26 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
env.users.users[mediaUnverifiedUserId] = user.User{ID: 3, Username: "pending", IsVerified: false}
|
||||
env.users.users[mediaStaleUserId] = user.User{ID: 4, Username: "stale", IsVerified: true, TokenVersion: 1}
|
||||
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage)
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, discardRecorder)
|
||||
now := func() time.Time { return env.clock }
|
||||
container := &api.UseCases{
|
||||
Token: env.token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(env.articles, &fakeBodyRenderer{}, discardRecorder),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(env.articles),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles, &fakeBodyRenderer{}, discardRecorder),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(env.articles, discardRecorder),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(env.users),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(env.images, env.storage),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(env.images, env.storage, discardRecorder),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(env.images, env.articles, env.users),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(env.images),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(env.images, env.storage),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(env.images, discardRecorder),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(env.images, env.storage, discardRecorder),
|
||||
MergeMediaUseCase: merge,
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(env.images, env.articles),
|
||||
GetPurgeSettingsUseCase: useCases.NewGetPurgeSettingsUseCase(env.purgeSettings),
|
||||
UpdatePurgeSettingsUseCase: useCases.NewUpdatePurgeSettingsUseCase(env.purgeSettings, now),
|
||||
PreviewPurgeUseCase: useCases.NewPreviewPurgeUseCase(env.images, env.articles),
|
||||
RunPurgeUseCase: useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, now, log.New(env.logs, "", 0)),
|
||||
RunPurgeUseCase: useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, now, log.New(env.logs, "", 0), discardRecorder),
|
||||
}
|
||||
router := chi.NewRouter()
|
||||
router.Mount("/v1", api.InitBackendRoutes(container, config.Settings{}))
|
||||
|
||||
@@ -947,9 +947,9 @@ func TestMediaMerging(t *testing.T) {
|
||||
|
||||
t.Run("A duplicated id reaching the use case is refused before anything is read", func(t *testing.T) {
|
||||
env := newEnv(t)
|
||||
uc := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage)
|
||||
uc := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, discardRecorder)
|
||||
|
||||
_, err := uc.Merge(2, []uint32{3, 3})
|
||||
_, err := uc.Merge(testCtx, 2, []uint32{3, 3})
|
||||
|
||||
if !errors.Is(err, domainImage.ErrInvalidMergeRequest) {
|
||||
t.Errorf("expected ErrInvalidMergeRequest, got %v", err)
|
||||
@@ -996,10 +996,10 @@ func TestMediaMerging(t *testing.T) {
|
||||
|
||||
func TestMediaSentinels(t *testing.T) {
|
||||
t.Run("Create reports unreadable bytes as ErrImageUnreadable", func(t *testing.T) {
|
||||
uc := useCases.NewCreateImageUseCase(newMockImageRepo(), newMockImageStorage())
|
||||
uc := useCases.NewCreateImageUseCase(newMockImageRepo(), newMockImageStorage(), discardRecorder)
|
||||
|
||||
for _, contentType := range domainImage.AllowedContentTypes() {
|
||||
_, err := uc.CreateImage(inputOf([]byte("not an image"), contentType))
|
||||
_, err := uc.CreateImage(testCtx, inputOf([]byte("not an image"), contentType))
|
||||
if !errors.Is(err, domainImage.ErrImageUnreadable) {
|
||||
t.Errorf("%s: expected ErrImageUnreadable, got %v", contentType, err)
|
||||
}
|
||||
@@ -1007,9 +1007,9 @@ func TestMediaSentinels(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("An svg without an svg root is unreadable", func(t *testing.T) {
|
||||
uc := useCases.NewCreateImageUseCase(newMockImageRepo(), newMockImageStorage())
|
||||
uc := useCases.NewCreateImageUseCase(newMockImageRepo(), newMockImageStorage(), discardRecorder)
|
||||
|
||||
_, err := uc.CreateImage(inputOf([]byte("<html/>"), "image/svg+xml"))
|
||||
_, err := uc.CreateImage(testCtx, inputOf([]byte("<html/>"), "image/svg+xml"))
|
||||
|
||||
if !errors.Is(err, domainImage.ErrImageUnreadable) {
|
||||
t.Errorf("expected ErrImageUnreadable, got %v", err)
|
||||
@@ -1017,9 +1017,9 @@ func TestMediaSentinels(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Bytes of another raster format than the declared type are unreadable", func(t *testing.T) {
|
||||
uc := useCases.NewCreateImageUseCase(newMockImageRepo(), newMockImageStorage())
|
||||
uc := useCases.NewCreateImageUseCase(newMockImageRepo(), newMockImageStorage(), discardRecorder)
|
||||
|
||||
_, err := uc.CreateImage(inputOf(jpegBytes(t, 3, 2), "image/png"))
|
||||
_, err := uc.CreateImage(testCtx, inputOf(jpegBytes(t, 3, 2), "image/png"))
|
||||
|
||||
if !errors.Is(err, domainImage.ErrImageUnreadable) {
|
||||
t.Errorf("expected ErrImageUnreadable, got %v", err)
|
||||
@@ -1029,9 +1029,9 @@ func TestMediaSentinels(t *testing.T) {
|
||||
t.Run("Rename reports an invalid stem as ErrInvalidFilename", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
repo.images[1] = domainImage.Image{ID: 1, Filename: "a.png"}
|
||||
uc := useCases.NewRenameMediaUseCase(repo)
|
||||
uc := useCases.NewRenameMediaUseCase(repo, discardRecorder)
|
||||
|
||||
if _, err := uc.Rename(1, "a b"); !errors.Is(err, domainImage.ErrInvalidFilename) {
|
||||
if _, err := uc.Rename(testCtx, 1, "a b"); !errors.Is(err, domainImage.ErrInvalidFilename) {
|
||||
t.Errorf("expected ErrInvalidFilename, got %v", err)
|
||||
}
|
||||
})
|
||||
@@ -1042,15 +1042,15 @@ func TestMediaSentinels(t *testing.T) {
|
||||
env.seedImage(2, "b.png", 1, "image/png", 1, 1, hashB, 1)
|
||||
env.seedImage(3, "c.png", 1, "image/png", 1, 1, "", 1)
|
||||
env.seedImage(4, "d.png", 1, "image/png", 1, 1, "", 1)
|
||||
uc := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage)
|
||||
uc := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, discardRecorder)
|
||||
|
||||
if _, err := uc.Merge(1, []uint32{9999}); !errors.Is(err, domainImage.ErrMergeCopyMissing) {
|
||||
if _, err := uc.Merge(testCtx, 1, []uint32{9999}); !errors.Is(err, domainImage.ErrMergeCopyMissing) {
|
||||
t.Errorf("expected ErrMergeCopyMissing, got %v", err)
|
||||
}
|
||||
if _, err := uc.Merge(1, []uint32{2}); !errors.Is(err, domainImage.ErrMergeHashMismatch) {
|
||||
if _, err := uc.Merge(testCtx, 1, []uint32{2}); !errors.Is(err, domainImage.ErrMergeHashMismatch) {
|
||||
t.Errorf("expected ErrMergeHashMismatch, got %v", err)
|
||||
}
|
||||
if _, err := uc.Merge(3, []uint32{4}); !errors.Is(err, domainImage.ErrMergeHashMismatch) {
|
||||
if _, err := uc.Merge(testCtx, 3, []uint32{4}); !errors.Is(err, domainImage.ErrMergeHashMismatch) {
|
||||
t.Errorf("expected images without a hash never to merge, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,13 +31,13 @@ type schedulerEnv struct {
|
||||
func newSchedulerEnv(t *testing.T) *schedulerEnv {
|
||||
t.Helper()
|
||||
env := newPurgeEnv(t)
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage)
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, discardRecorder)
|
||||
now := func() time.Time { return env.clock }
|
||||
runPurge := useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, now, log.New(env.logs, "", 0))
|
||||
runPurge := useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, now, log.New(env.logs, "", 0), discardRecorder)
|
||||
return &schedulerEnv{
|
||||
mediaEnv: env,
|
||||
runPurge: runPurge,
|
||||
scheduler: useCases.NewPurgeSchedulerUseCase(env.purgeSettings, runPurge),
|
||||
scheduler: useCases.NewPurgeSchedulerUseCase(env.purgeSettings, runPurge, discardRecorder),
|
||||
update: useCases.NewUpdatePurgeSettingsUseCase(env.purgeSettings, now),
|
||||
}
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func TestPurgeRunKeepsSettingsChangedWhileItRuns(t *testing.T) {
|
||||
}
|
||||
runDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := env.runPurge.Run(nil)
|
||||
_, err := env.runPurge.Run(testCtx, nil)
|
||||
runDone <- err
|
||||
}()
|
||||
waitFor(t, writing)
|
||||
@@ -430,7 +430,7 @@ func TestPurgeSchedulerFailures(t *testing.T) {
|
||||
}
|
||||
manualDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := env.runPurge.Run(nil)
|
||||
_, err := env.runPurge.Run(testCtx, nil)
|
||||
manualDone <- err
|
||||
}()
|
||||
waitFor(t, entered)
|
||||
|
||||
+10
-10
@@ -32,8 +32,8 @@ func TestUpdatePurgeSettingsRejectsOutOfRangeIntervals(t *testing.T) {
|
||||
|
||||
func TestRunPurgeDistinguishesBusyFromConflict(t *testing.T) {
|
||||
newRun := func(env *mediaEnv) *useCases.RunPurgeUseCase {
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage)
|
||||
return useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, func() time.Time { return env.clock }, log.New(&bytes.Buffer{}, "", 0))
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, discardRecorder)
|
||||
return useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, func() time.Time { return env.clock }, log.New(&bytes.Buffer{}, "", 0), discardRecorder)
|
||||
}
|
||||
|
||||
t.Run("a non-matching fingerprint is a conflict and writes nothing", func(t *testing.T) {
|
||||
@@ -42,7 +42,7 @@ func TestRunPurgeDistinguishesBusyFromConflict(t *testing.T) {
|
||||
env.useImages(10, 1)
|
||||
wrong := "wrong"
|
||||
|
||||
_, err := newRun(env).Run(&wrong)
|
||||
_, err := newRun(env).Run(testCtx, &wrong)
|
||||
|
||||
if !errors.Is(err, purge.ErrPurgeConflict) || errors.Is(err, purge.ErrPurgeBusy) {
|
||||
t.Errorf("expected only ErrPurgeConflict, got %v", err)
|
||||
@@ -68,13 +68,13 @@ func TestRunPurgeDistinguishesBusyFromConflict(t *testing.T) {
|
||||
run := newRun(env)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := run.Run(nil)
|
||||
_, err := run.Run(testCtx, nil)
|
||||
done <- err
|
||||
}()
|
||||
<-entered
|
||||
savesBefore := env.purgeSettings.saveCalls
|
||||
|
||||
_, err := run.Run(nil)
|
||||
_, err := run.Run(testCtx, nil)
|
||||
|
||||
if !errors.Is(err, purge.ErrPurgeBusy) || errors.Is(err, purge.ErrPurgeConflict) {
|
||||
t.Errorf("expected only ErrPurgeBusy, got %v", err)
|
||||
@@ -93,7 +93,7 @@ func TestRunPurgeDistinguishesBusyFromConflict(t *testing.T) {
|
||||
env.seedGroup(hashA, 1, 2)
|
||||
env.useImages(10, 1)
|
||||
|
||||
result, err := newRun(env).Run(nil)
|
||||
result, err := newRun(env).Run(testCtx, nil)
|
||||
|
||||
if err != nil || result != (purge.RunResult{GroupsTotal: 1, GroupsPurged: 1, Merged: 1}) {
|
||||
t.Errorf("unexpected result %+v, %v", result, err)
|
||||
@@ -148,9 +148,9 @@ func TestMergeMediaExposesTheRewrittenArticleIds(t *testing.T) {
|
||||
env.useImages(1, 2)
|
||||
env.useImages(2, 1, 2)
|
||||
env.useImages(3, 1)
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage)
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, discardRecorder)
|
||||
|
||||
result, err := merge.Merge(1, []uint32{2})
|
||||
result, err := merge.Merge(testCtx, 1, []uint32{2})
|
||||
|
||||
if err != nil || result.Merged != 1 || result.ArticlesUpdated != 2 || !reflect.DeepEqual(result.UpdatedArticleIDs, []uint32{1, 2}) {
|
||||
t.Errorf("unexpected result %+v, %v", result, err)
|
||||
@@ -163,9 +163,9 @@ func TestMergeMediaExposesTheRewrittenArticleIds(t *testing.T) {
|
||||
env.useImages(1, 2)
|
||||
env.useImages(2, 2)
|
||||
env.articles.failUpdateBodyFor = map[uint32]error{2: errRepositoryFailure}
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage)
|
||||
merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, discardRecorder)
|
||||
|
||||
result, err := merge.Merge(1, []uint32{2})
|
||||
result, err := merge.Merge(testCtx, 1, []uint32{2})
|
||||
|
||||
if err == nil || result.Merged != 0 || result.ArticlesUpdated != 0 || len(result.UpdatedArticleIDs) != 0 {
|
||||
t.Errorf("unexpected result %+v, %v", result, err)
|
||||
|
||||
@@ -24,7 +24,7 @@ func pngUpload(t *testing.T) domainImage.ImageInput {
|
||||
|
||||
func uploadPng(t *testing.T, uc *useCases.CreateImageUseCase) domainImage.Image {
|
||||
t.Helper()
|
||||
created, err := uc.CreateImage(pngUpload(t))
|
||||
created, err := uc.CreateImage(testCtx, pngUpload(t))
|
||||
if err != nil {
|
||||
t.Fatalf("CreateImage failed: %v", err)
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func TestCreateImageStoresTheBlobThenTheRow(t *testing.T) {
|
||||
t.Run("The row points at the stored name and the blob holds the bytes", func(t *testing.T) {
|
||||
repo, store := newMockImageRepo(), newMockImageStorage()
|
||||
|
||||
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store))
|
||||
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store, discardRecorder))
|
||||
|
||||
if !strings.HasPrefix(created.Path, domainImage.PublicPathPrefix) {
|
||||
t.Errorf("unexpected path %q", created.Path)
|
||||
@@ -49,7 +49,7 @@ func TestCreateImageStoresTheBlobThenTheRow(t *testing.T) {
|
||||
repo, store := newMockImageRepo(), newMockImageStorage()
|
||||
store.failPut = errRepositoryFailure
|
||||
|
||||
_, err := useCases.NewCreateImageUseCase(repo, store).CreateImage(pngUpload(t))
|
||||
_, err := useCases.NewCreateImageUseCase(repo, store, discardRecorder).CreateImage(testCtx, pngUpload(t))
|
||||
|
||||
if !errors.Is(err, errRepositoryFailure) || repo.createCalls != 0 {
|
||||
t.Errorf("got %v with %d Create calls", err, repo.createCalls)
|
||||
@@ -60,7 +60,7 @@ func TestCreateImageStoresTheBlobThenTheRow(t *testing.T) {
|
||||
repo, store := newMockImageRepo(), newMockImageStorage()
|
||||
repo.fail.failNextWith("Create", errRepositoryFailure)
|
||||
|
||||
_, err := useCases.NewCreateImageUseCase(repo, store).CreateImage(pngUpload(t))
|
||||
_, err := useCases.NewCreateImageUseCase(repo, store, discardRecorder).CreateImage(testCtx, pngUpload(t))
|
||||
|
||||
if !errors.Is(err, errRepositoryFailure) {
|
||||
t.Fatalf("expected the repository failure, got %v", err)
|
||||
@@ -74,9 +74,9 @@ func TestCreateImageStoresTheBlobThenTheRow(t *testing.T) {
|
||||
func TestDeleteMediaRemovesTheBlobAndTheRow(t *testing.T) {
|
||||
t.Run("Both disappear", func(t *testing.T) {
|
||||
repo, store := newMockImageRepo(), newMockImageStorage()
|
||||
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store))
|
||||
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store, discardRecorder))
|
||||
|
||||
if err := useCases.NewDeleteMediaUseCase(repo, store).Delete(created.ID); err != nil {
|
||||
if err := useCases.NewDeleteMediaUseCase(repo, store, discardRecorder).Delete(testCtx, created.ID); err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
if len(store.blobs) != 0 || len(repo.images) != 0 {
|
||||
@@ -87,7 +87,7 @@ func TestDeleteMediaRemovesTheBlobAndTheRow(t *testing.T) {
|
||||
t.Run("An unknown id is ErrImageNotFound and touches no blob", func(t *testing.T) {
|
||||
repo, store := newMockImageRepo(), newMockImageStorage()
|
||||
|
||||
err := useCases.NewDeleteMediaUseCase(repo, store).Delete(7)
|
||||
err := useCases.NewDeleteMediaUseCase(repo, store, discardRecorder).Delete(testCtx, 7)
|
||||
|
||||
if !errors.Is(err, domainImage.ErrImageNotFound) || len(store.deleted) != 0 {
|
||||
t.Errorf("got %v, deleted %v", err, store.deleted)
|
||||
@@ -96,10 +96,10 @@ func TestDeleteMediaRemovesTheBlobAndTheRow(t *testing.T) {
|
||||
|
||||
t.Run("A blob failure keeps the row so the delete can be retried", func(t *testing.T) {
|
||||
repo, store := newMockImageRepo(), newMockImageStorage()
|
||||
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store))
|
||||
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store, discardRecorder))
|
||||
store.failDelete = errRepositoryFailure
|
||||
|
||||
err := useCases.NewDeleteMediaUseCase(repo, store).Delete(created.ID)
|
||||
err := useCases.NewDeleteMediaUseCase(repo, store, discardRecorder).Delete(testCtx, created.ID)
|
||||
|
||||
if !errors.Is(err, errRepositoryFailure) || len(repo.images) != 1 || len(repo.deleteCalls) != 0 {
|
||||
t.Errorf("got %v, rows=%d deleteCalls=%v", err, len(repo.images), repo.deleteCalls)
|
||||
@@ -109,10 +109,10 @@ func TestDeleteMediaRemovesTheBlobAndTheRow(t *testing.T) {
|
||||
|
||||
func TestMergeMediaRemovesTheMergedBlobs(t *testing.T) {
|
||||
repo, articles, store := newMockImageRepo(), newMockArticleRepo(), newMockImageStorage()
|
||||
create := useCases.NewCreateImageUseCase(repo, store)
|
||||
create := useCases.NewCreateImageUseCase(repo, store, discardRecorder)
|
||||
kept, copyOne, copyTwo := uploadPng(t, create), uploadPng(t, create), uploadPng(t, create)
|
||||
|
||||
result, err := useCases.NewMergeMediaUseCase(repo, articles, store).Merge(kept.ID, []uint32{copyOne.ID, copyTwo.ID})
|
||||
result, err := useCases.NewMergeMediaUseCase(repo, articles, store, discardRecorder).Merge(testCtx, kept.ID, []uint32{copyOne.ID, copyTwo.ID})
|
||||
|
||||
if err != nil || result.Merged != 2 {
|
||||
t.Fatalf("merge returned %+v, %v", result, err)
|
||||
|
||||
@@ -126,17 +126,17 @@ func TestUserRepositoryReportsMissingUserAsDomainError(t *testing.T) {
|
||||
func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -42,11 +42,11 @@ func newPublicArticleListingRouter(articleRepo *mockArticleRepo, token *jwtauth.
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
func newPublicArticleReadRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ func newRegistrationGateRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, p
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, publicURL),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, publicURL, discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
@@ -218,9 +218,9 @@ func TestRegistrationGateAndVerificationEmailUseCase(t *testing.T) {
|
||||
userRepo.nextID = 1
|
||||
userRepo.fail.failNext("Create")
|
||||
mailRepo := newMockMailRepo()
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
|
||||
_, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
_, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "bob",
|
||||
Password: "hunter2example12",
|
||||
Email: "bob@example.com",
|
||||
@@ -240,9 +240,9 @@ func TestRegistrationGateAndVerificationEmailUseCase(t *testing.T) {
|
||||
userRepo.users[2] = user.User{ID: 2, Username: "bob", Email: "bob@example.com", IsVerified: false}
|
||||
userRepo.nextID = 2
|
||||
mailRepo := newMockMailRepo()
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
|
||||
result, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
result, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "carol",
|
||||
Password: "hunter2example12",
|
||||
Email: "carol@example.com",
|
||||
@@ -268,9 +268,9 @@ func TestRegistrationGateAndVerificationEmailUseCase(t *testing.T) {
|
||||
t.Run("A failure hashing the verification code persists nothing", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
mailRepo := newMockMailRepo()
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &sequentialHasher{}, mailRepo, "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &sequentialHasher{}, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
|
||||
_, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
_, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "alice",
|
||||
Password: "hunter2example12",
|
||||
Email: "alice@example.com",
|
||||
@@ -291,9 +291,9 @@ func TestRegistrationGateAndVerificationEmailUseCase(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
mailRepo := newMockMailRepo()
|
||||
mailRepo.fail.failNext("Send")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
|
||||
_, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
_, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "alice",
|
||||
Password: "hunter2example12",
|
||||
Email: "alice@example.com",
|
||||
@@ -312,9 +312,9 @@ func TestRegistrationGateAndVerificationEmailUseCase(t *testing.T) {
|
||||
mailRepo := newMockMailRepo()
|
||||
mailRepo.fail.failNext("Send")
|
||||
userRepo.fail.failNext("Delete")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
|
||||
_, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
_, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "alice",
|
||||
Password: "hunter2example12",
|
||||
Email: "alice@example.com",
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func newRegistrationStatusRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo) (*chi.Mux, *useCases.CreateUserUseCase) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
hasher := &stubHasher{}
|
||||
createUseCase := useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com")
|
||||
createUseCase := useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
loginHandler := auth.NewLoginHandler(token, createUseCase, useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder), true)
|
||||
|
||||
r := chi.NewRouter()
|
||||
@@ -162,13 +162,13 @@ func TestRegistrationStatusEndpointHttp(t *testing.T) {
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: true}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
|
||||
if uc.IsRegistrationOpen() {
|
||||
t.Fatalf("expected IsRegistrationOpen to return false")
|
||||
}
|
||||
|
||||
_, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
_, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "bob",
|
||||
Password: "hunter2example12",
|
||||
Email: "bob@example.com",
|
||||
|
||||
@@ -35,7 +35,7 @@ func newSecureCookieAuthRouter(t *testing.T, isProduction bool) (*chi.Mux, *mock
|
||||
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
isProduction,
|
||||
)
|
||||
@@ -129,22 +129,22 @@ func newRuntimeSettingsContainer(t *testing.T) *api.UseCases {
|
||||
|
||||
return &api.UseCases{
|
||||
Token: token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(userRepo),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
ListUsersUseCase: useCases.NewListUsersUseCase(userRepo),
|
||||
SendMailUseCase: useCases.NewSendMailUseCase(mailRepo),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(imageRepo, newMockImageStorage()),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(imageRepo, newMockImageStorage(), discardRecorder),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(imageRepo, articleRepo, userRepo),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(imageRepo),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(imageRepo, newMockImageStorage()),
|
||||
MergeMediaUseCase: useCases.NewMergeMediaUseCase(imageRepo, articleRepo, newMockImageStorage()),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(imageRepo, discardRecorder),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(imageRepo, newMockImageStorage(), discardRecorder),
|
||||
MergeMediaUseCase: useCases.NewMergeMediaUseCase(imageRepo, articleRepo, newMockImageStorage(), discardRecorder),
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(imageRepo, articleRepo),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,21 +23,21 @@ import (
|
||||
func newStaleTokenInvalidationRouter(userRepo *mockUserRepo, articleRepo *mockArticleRepo, imageRepo *mockImageRepo, mailRepo *mockMailRepo, hasher *stubHasher, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher),
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder),
|
||||
useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ func newTrustedProxyAuthRouter(t *testing.T, userRepo user.Repository, mailRepo
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -155,7 +155,7 @@ func newUnverifiedRegistrationRouter(userRepo *mockUserRepo, token *jwtauth.JWTA
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -39,10 +39,10 @@ func newDownloadHTTP(t *testing.T, rig *downloadRig) *updateHarness {
|
||||
Token: token,
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(users),
|
||||
GetUpdateStatusUseCase: rig.status,
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, rig.store, updateRunning, now),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, rig.store, updateRunning, now, discardRecorder),
|
||||
StartUpdateDownloadUseCase: rig.start,
|
||||
}
|
||||
return &updateHarness{router: api.NewSystemRouter(container), token: token, feed: feed, store: rig.store}
|
||||
return &updateHarness{router: api.NewSystemRouter(container, nil), token: token, feed: feed, store: rig.store}
|
||||
}
|
||||
|
||||
func waitForHTTPState(t *testing.T, h *updateHarness, states ...string) map[string]any {
|
||||
@@ -597,7 +597,7 @@ func TestFailedUpdateJobRemovesItsStagedDirectoryEvenWhenRemovalFails(t *testing
|
||||
rig.fetcher.errors[rig.assetName] = errDownloadNetwork
|
||||
rig.staging.removeErr = errDownloadDisk
|
||||
|
||||
if _, err := rig.start.Start(); err != nil {
|
||||
if _, err := rig.start.Start(testCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
job := rig.waitForJob(update.StateFailed)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/update"
|
||||
"RenewCMS/internal/infrastructure/persistence/database"
|
||||
"RenewCMS/internal/infrastructure/persistence/models"
|
||||
@@ -313,6 +314,7 @@ type downloadRig struct {
|
||||
staging *faultyStagingArea
|
||||
realBackup update.DatabaseBackup
|
||||
logger *fakeUpdateLogger
|
||||
recorder audit.Recorder
|
||||
start *useCases.StartUpdateDownloadUseCase
|
||||
status *useCases.GetUpdateStatusUseCase
|
||||
}
|
||||
@@ -385,10 +387,18 @@ func (r *downloadRig) rebuild() {
|
||||
Platform: r.platform,
|
||||
FeedURL: downloadFeedURL,
|
||||
Now: func() time.Time { return updateTestCheckedAt },
|
||||
Recorder: r.auditRecorder(),
|
||||
})
|
||||
r.status = useCases.NewGetUpdateStatusUseCase(r.store, r.jobs, r.helper, r.results, r.requests, updateRunning, false, time.Now)
|
||||
}
|
||||
|
||||
func (r *downloadRig) auditRecorder() audit.Recorder {
|
||||
if r.recorder == nil {
|
||||
return discardRecorder
|
||||
}
|
||||
return r.recorder
|
||||
}
|
||||
|
||||
func (r *downloadRig) checksumsFor(name string, content []byte) string {
|
||||
sum := sha256.Sum256(content)
|
||||
return hex.EncodeToString(sum[:]) + " " + name + "\n"
|
||||
@@ -484,7 +494,7 @@ func TestAssetNameFollowsThePlatform(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := rig.start.Start(); err != nil {
|
||||
if _, err := rig.start.Start(testCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rig.waitForJob(update.StateReady, update.StateFailed)
|
||||
@@ -543,7 +553,7 @@ func TestStartUpdateDownloadUseCaseHappyPath(t *testing.T) {
|
||||
rig.backup.gate = gate
|
||||
rig.backup.entered = entered
|
||||
|
||||
status, err := rig.start.Start()
|
||||
status, err := rig.start.Start(testCtx)
|
||||
|
||||
if err != nil || status.State != update.StateDownloading || status.StartedAt == nil {
|
||||
t.Fatalf("status = %+v, err = %v", status, err)
|
||||
@@ -582,7 +592,7 @@ func TestStartUpdateDownloadUseCaseGuards(t *testing.T) {
|
||||
t.Run("without the helper nothing starts", func(t *testing.T) {
|
||||
rig := newDownloadRig(t)
|
||||
rig.helper.installed = false
|
||||
if _, err := rig.start.Start(); !errors.Is(err, update.ErrNotPrepared) {
|
||||
if _, err := rig.start.Start(testCtx); !errors.Is(err, update.ErrNotPrepared) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
if rig.fetcher.totalCalls() != 0 {
|
||||
@@ -594,7 +604,7 @@ func TestStartUpdateDownloadUseCaseGuards(t *testing.T) {
|
||||
rig := newDownloadRig(t)
|
||||
rig.helper.installed = false
|
||||
rig.store.found = false
|
||||
if _, err := rig.start.Start(); !errors.Is(err, update.ErrNotPrepared) {
|
||||
if _, err := rig.start.Start(testCtx); !errors.Is(err, update.ErrNotPrepared) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
@@ -603,7 +613,7 @@ func TestStartUpdateDownloadUseCaseGuards(t *testing.T) {
|
||||
rig := newDownloadRig(t)
|
||||
rig.jobs.put(update.Job{State: update.StateReady})
|
||||
rig.store.found = false
|
||||
if _, err := rig.start.Start(); !errors.Is(err, update.ErrJobInProgress) {
|
||||
if _, err := rig.start.Start(testCtx); !errors.Is(err, update.ErrJobInProgress) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
@@ -611,7 +621,7 @@ func TestStartUpdateDownloadUseCaseGuards(t *testing.T) {
|
||||
t.Run("nothing stored", func(t *testing.T) {
|
||||
rig := newDownloadRig(t)
|
||||
rig.store.found = false
|
||||
if _, err := rig.start.Start(); !errors.Is(err, update.ErrNothingToUpdate) {
|
||||
if _, err := rig.start.Start(testCtx); !errors.Is(err, update.ErrNothingToUpdate) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
@@ -619,7 +629,7 @@ func TestStartUpdateDownloadUseCaseGuards(t *testing.T) {
|
||||
t.Run("a job record that cannot be saved is an error", func(t *testing.T) {
|
||||
rig := newDownloadRig(t)
|
||||
rig.jobs.saveErr = errRepositoryFailure
|
||||
if _, err := rig.start.Start(); !errors.Is(err, errRepositoryFailure) {
|
||||
if _, err := rig.start.Start(testCtx); !errors.Is(err, errRepositoryFailure) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
if rig.fetcher.totalCalls() != 0 {
|
||||
@@ -643,7 +653,7 @@ func TestTwoStartsAtOnceRunOneJob(t *testing.T) {
|
||||
go func() {
|
||||
ready.Done()
|
||||
ready.Wait()
|
||||
_, err := rig.start.Start()
|
||||
_, err := rig.start.Start(testCtx)
|
||||
results <- err
|
||||
}()
|
||||
}
|
||||
@@ -922,7 +932,7 @@ func TestStartUpdateDownloadUseCaseProgressCadence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := rig.start.Start(); err != nil {
|
||||
if _, err := rig.start.Start(testCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rig.waitForJob(update.StateReady, update.StateFailed)
|
||||
@@ -938,7 +948,7 @@ func TestStartUpdateDownloadUseCaseAcceptsAnUndeclaredSize(t *testing.T) {
|
||||
rig := newDownloadRig(t)
|
||||
rig.setAsset(rig.assetName, func(asset *update.Asset) { asset.Size = 0 })
|
||||
|
||||
if _, err := rig.start.Start(); err != nil {
|
||||
if _, err := rig.start.Start(testCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -961,7 +971,7 @@ func TestStartUpdateDownloadUseCaseReportsAFailedRecordSave(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := rig.start.Start(); err != nil {
|
||||
if _, err := rig.start.Start(testCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -978,7 +988,7 @@ func TestStartUpdateDownloadUseCaseReportsAFailedRecordSave(t *testing.T) {
|
||||
rig := newDownloadRig(t)
|
||||
rig.fetcher.errors[rig.assetName] = errDownloadNetwork
|
||||
|
||||
if _, err := rig.start.Start(); err != nil {
|
||||
if _, err := rig.start.Start(testCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rig.waitForJob(update.StateFailed)
|
||||
|
||||
@@ -64,9 +64,9 @@ func newUpdateHarness(t *testing.T, running string) *updateHarness {
|
||||
Token: token,
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(users),
|
||||
GetUpdateStatusUseCase: newUpdateStatusUseCase(store, &fakeJobStore{}, &fakeHelperDetector{}, running),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, store, running, now),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, store, running, now, discardRecorder),
|
||||
}
|
||||
return &updateHarness{router: api.NewSystemRouter(container), token: token, feed: feed, store: store}
|
||||
return &updateHarness{router: api.NewSystemRouter(container, nil), token: token, feed: feed, store: store}
|
||||
}
|
||||
|
||||
func (h *updateHarness) send(t *testing.T, method, path string, userId uint32, tokenVersion uint32) *httptest.ResponseRecorder {
|
||||
|
||||
@@ -61,9 +61,9 @@ func newRestartHTTP(t *testing.T) *restartHTTP {
|
||||
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(
|
||||
h.store, h.jobs, h.helper, h.resultFiles, h.markers, updateRunning, false, h.clock.Now),
|
||||
RequestRestartUseCase: useCases.NewRequestRestartUseCase(
|
||||
h.store, h.jobs, h.helper, h.resultFiles, h.markers, updateRunning, h.clock.Now),
|
||||
h.store, h.jobs, h.helper, h.resultFiles, h.markers, updateRunning, h.clock.Now, discardRecorder),
|
||||
}
|
||||
h.router = api.NewSystemRouter(container)
|
||||
h.router = api.NewSystemRouter(container, nil)
|
||||
return h
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func newRestartRig(running string) *restartRig {
|
||||
requests: &fakeRestartRequestStore{},
|
||||
clock: &fakeClock{now: restartBaseTime},
|
||||
}
|
||||
rig.request = useCases.NewRequestRestartUseCase(rig.store, rig.jobs, rig.helper, rig.results, rig.requests, running, rig.clock.Now)
|
||||
rig.request = useCases.NewRequestRestartUseCase(rig.store, rig.jobs, rig.helper, rig.results, rig.requests, running, rig.clock.Now, discardRecorder)
|
||||
rig.status = useCases.NewGetUpdateStatusUseCase(rig.store, rig.jobs, rig.helper, rig.results, rig.requests, running, false, rig.clock.Now)
|
||||
return rig
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func TestRequestRestartUseCase(t *testing.T) {
|
||||
rig.readyJob()
|
||||
rig.results.put(update.Result{Outcome: update.OutcomeRolledBack})
|
||||
|
||||
status, err := rig.request.Request(updateOwnerId, "")
|
||||
status, err := rig.request.Request(testCtx, updateOwnerId, "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -81,7 +81,7 @@ func TestRequestRestartUseCase(t *testing.T) {
|
||||
rig := newRestartRig(updateRunning)
|
||||
prepare(rig)
|
||||
|
||||
status, err := rig.request.Request(updateOwnerId, update.RestartKindRestart)
|
||||
status, err := rig.request.Request(testCtx, updateOwnerId, update.RestartKindRestart)
|
||||
|
||||
marker, _ := rig.requests.Read()
|
||||
if err != nil || marker.Kind != update.RestartKindRestart || marker.Tag != "" || status.State != update.StateRestarting {
|
||||
@@ -107,7 +107,7 @@ func TestRequestRestartUseCase(t *testing.T) {
|
||||
tc.prepare(rig)
|
||||
jobBefore, _ := rig.jobs.Load()
|
||||
|
||||
_, err := rig.request.Request(updateOwnerId, tc.kind)
|
||||
_, err := rig.request.Request(testCtx, updateOwnerId, tc.kind)
|
||||
|
||||
if !errors.Is(err, update.ErrRestartKindMismatch) {
|
||||
t.Fatalf("err = %v", err)
|
||||
@@ -140,7 +140,7 @@ func TestRequestRestartUseCase(t *testing.T) {
|
||||
resultBefore, foundBefore := rig.results.Read()
|
||||
writesBefore := rig.requests.writes
|
||||
|
||||
_, err := rig.request.Request(updateOwnerId, "")
|
||||
_, err := rig.request.Request(testCtx, updateOwnerId, "")
|
||||
|
||||
resultAfter, foundAfter := rig.results.Read()
|
||||
if !errors.Is(err, tc.want) || rig.requests.writes != writesBefore || resultAfter != resultBefore || foundAfter != foundBefore || rig.results.deletes != 0 {
|
||||
@@ -154,7 +154,7 @@ func TestRequestRestartUseCase(t *testing.T) {
|
||||
rig := newRestartRig(updateRunning)
|
||||
rig.results.put(update.Result{Outcome: update.OutcomeRunning, StartedAt: restartBaseTime.Add(-6 * time.Minute)})
|
||||
|
||||
_, err := rig.request.Request(updateOwnerId, "")
|
||||
_, err := rig.request.Request(testCtx, updateOwnerId, "")
|
||||
|
||||
if err != nil || rig.requests.writes != 1 {
|
||||
t.Errorf("err = %v, writes = %d", err, rig.requests.writes)
|
||||
@@ -167,7 +167,7 @@ func TestRequestRestartUseCase(t *testing.T) {
|
||||
rig.requests.writeErr = errors.New("disk full at /var/lib/renewcms")
|
||||
jobBefore, _ := rig.jobs.Load()
|
||||
|
||||
_, err := rig.request.Request(updateOwnerId, "")
|
||||
_, err := rig.request.Request(testCtx, updateOwnerId, "")
|
||||
|
||||
jobAfter, _ := rig.jobs.Load()
|
||||
if err == nil || errors.Is(err, update.ErrRestartKindMismatch) || rig.requests.present || jobAfter != jobBefore {
|
||||
@@ -183,7 +183,7 @@ func TestRequestRestartUseCase(t *testing.T) {
|
||||
wait.Add(1)
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
_, errs[i] = rig.request.Request(uint32(i+1), "")
|
||||
_, errs[i] = rig.request.Request(testCtx, uint32(i+1), "")
|
||||
}()
|
||||
}
|
||||
wait.Wait()
|
||||
|
||||
+4
-4
@@ -199,7 +199,7 @@ func TestCheckForUpdateUseCase(t *testing.T) {
|
||||
store := &fakeUpdateStore{}
|
||||
feed := &fakeReleaseFeed{release: newRelease}
|
||||
|
||||
status, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now).Check()
|
||||
status, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now, discardRecorder).Check(testCtx)
|
||||
|
||||
if err != nil || !status.Available || status.Latest != "v2.3.0-beta" {
|
||||
t.Fatalf("status = %+v, err = %v", status, err)
|
||||
@@ -211,7 +211,7 @@ func TestCheckForUpdateUseCase(t *testing.T) {
|
||||
|
||||
t.Run("nothing newer", func(t *testing.T) {
|
||||
feed := &fakeReleaseFeed{release: update.Release{Tag: "v2.2.0-beta"}}
|
||||
status, err := useCases.NewCheckForUpdateUseCase(feed, &fakeUpdateStore{}, "v2.2.0-beta", now).Check()
|
||||
status, err := useCases.NewCheckForUpdateUseCase(feed, &fakeUpdateStore{}, "v2.2.0-beta", now, discardRecorder).Check(testCtx)
|
||||
if err != nil || status.Available {
|
||||
t.Errorf("status = %+v, err = %v", status, err)
|
||||
}
|
||||
@@ -221,7 +221,7 @@ func TestCheckForUpdateUseCase(t *testing.T) {
|
||||
store := &fakeUpdateStore{result: newStoredUpdateResult(), found: true}
|
||||
feed := &fakeReleaseFeed{err: update.ErrFeedUnavailable}
|
||||
|
||||
_, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now).Check()
|
||||
_, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now, discardRecorder).Check(testCtx)
|
||||
|
||||
if !errors.Is(err, update.ErrFeedUnavailable) {
|
||||
t.Errorf("err = %v", err)
|
||||
@@ -234,7 +234,7 @@ func TestCheckForUpdateUseCase(t *testing.T) {
|
||||
t.Run("store failure is returned", func(t *testing.T) {
|
||||
store := &fakeUpdateStore{saveErr: errRepositoryFailure}
|
||||
feed := &fakeReleaseFeed{release: newRelease}
|
||||
if _, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now).Check(); !errors.Is(err, errRepositoryFailure) {
|
||||
if _, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now, discardRecorder).Check(testCtx); !errors.Is(err, errRepositoryFailure) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,10 +29,10 @@ func TestCreateArticleTitleLengthLimit(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("length_%d", tc.length), func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
|
||||
title := strings.Repeat("a", tc.length)
|
||||
result, err := uc.CreateArticle(useCases.CreateArticleCommand{Title: title, Body: "Some body text"})
|
||||
result, err := uc.CreateArticle(testCtx, useCases.CreateArticleCommand{Title: title, Body: "Some body text"})
|
||||
|
||||
if tc.expectValid {
|
||||
if err != nil {
|
||||
@@ -62,9 +62,9 @@ func TestCreateArticleTitleLengthLimit(t *testing.T) {
|
||||
func TestCreateArticlePropagatesRepositoryFailure(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
repo.fail.failNext("Create")
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
|
||||
_, err := uc.CreateArticle(useCases.CreateArticleCommand{Title: "Backyard Renovation Update", Body: "Some body text"})
|
||||
_, err := uc.CreateArticle(testCtx, useCases.CreateArticleCommand{Title: "Backyard Renovation Update", Body: "Some body text"})
|
||||
|
||||
if !errors.Is(err, errRepositoryFailure) {
|
||||
t.Errorf("expected repository failure, got %v", err)
|
||||
@@ -242,8 +242,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateBody_id1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.UpdateBody(1, "New body", article.BodyFormatHTML)
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
result, err := uc.UpdateBody(testCtx, 1, "New body", article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -254,8 +254,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateBody_id9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.UpdateBody(9, "New body", article.BodyFormatHTML)
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
_, err := uc.UpdateBody(testCtx, 9, "New body", article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
@@ -263,8 +263,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateIsOnline_id1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.UpdateIsOnline(1, true, article.BodyFormatHTML)
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
result, err := uc.UpdateIsOnline(testCtx, 1, true, article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -275,8 +275,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateIsOnline_id9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.UpdateIsOnline(9, true, article.BodyFormatHTML)
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder)
|
||||
_, err := uc.UpdateIsOnline(testCtx, 9, true, article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
@@ -287,9 +287,9 @@ func TestDeleteArticleReportsMissingArticle(t *testing.T) {
|
||||
t.Run("id_1", func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
repo.articles[1] = article.Article{ID: 1}
|
||||
uc := useCases.NewDeleteArticleUseCase(repo)
|
||||
uc := useCases.NewDeleteArticleUseCase(repo, discardRecorder)
|
||||
|
||||
err := uc.DeleteArticle(1)
|
||||
err := uc.DeleteArticle(testCtx, 1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -298,9 +298,9 @@ func TestDeleteArticleReportsMissingArticle(t *testing.T) {
|
||||
t.Run("id_9", func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
repo.articles[1] = article.Article{ID: 1}
|
||||
uc := useCases.NewDeleteArticleUseCase(repo)
|
||||
uc := useCases.NewDeleteArticleUseCase(repo, discardRecorder)
|
||||
|
||||
err := uc.DeleteArticle(9)
|
||||
err := uc.DeleteArticle(testCtx, 9)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
@@ -311,9 +311,9 @@ func TestDeleteArticlePropagatesRepositoryFailure(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
repo.articles[1] = article.Article{ID: 1}
|
||||
repo.fail.failNext("Delete")
|
||||
uc := useCases.NewDeleteArticleUseCase(repo)
|
||||
uc := useCases.NewDeleteArticleUseCase(repo, discardRecorder)
|
||||
|
||||
err := uc.DeleteArticle(1)
|
||||
err := uc.DeleteArticle(testCtx, 1)
|
||||
if !errors.Is(err, errRepositoryFailure) {
|
||||
t.Errorf("expected repository failure, got %v", err)
|
||||
}
|
||||
@@ -334,13 +334,13 @@ func TestCreateImageDelegatesContentTypeCheck(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(strings.ReplaceAll(tc.contentType, "/", "_"), func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
uc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
uc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
content := []byte("fake-image-data")
|
||||
if tc.valid {
|
||||
content = validBytesFor(t, tc.contentType)
|
||||
}
|
||||
result, err := uc.CreateImage(imageDomain.ImageInput{
|
||||
result, err := uc.CreateImage(testCtx, imageDomain.ImageInput{
|
||||
Content: bytes.NewReader(content),
|
||||
Filename: "test.img",
|
||||
ContentType: tc.contentType,
|
||||
@@ -368,9 +368,9 @@ func TestCreateImageDelegatesContentTypeCheck(t *testing.T) {
|
||||
func TestCreateImagePropagatesRepositoryFailure(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
repo.fail.failNext("Create")
|
||||
uc := useCases.NewCreateImageUseCase(repo, newMockImageStorage())
|
||||
uc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
_, err := uc.CreateImage(imageDomain.ImageInput{
|
||||
_, err := uc.CreateImage(testCtx, imageDomain.ImageInput{
|
||||
Content: bytes.NewReader(pngBytes(t, 3, 2)),
|
||||
Filename: "test.png",
|
||||
ContentType: "image/png",
|
||||
@@ -385,9 +385,9 @@ func TestDeleteImageRequiresExistence(t *testing.T) {
|
||||
t.Run("id_1", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
repo.images[1] = imageDomain.Image{ID: 1}
|
||||
uc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage())
|
||||
uc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
err := uc.Delete(1)
|
||||
err := uc.Delete(testCtx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -399,9 +399,9 @@ func TestDeleteImageRequiresExistence(t *testing.T) {
|
||||
t.Run("id_9", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
repo.images[1] = imageDomain.Image{ID: 1}
|
||||
uc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage())
|
||||
uc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage(), discardRecorder)
|
||||
|
||||
err := uc.Delete(9)
|
||||
err := uc.Delete(testCtx, 9)
|
||||
if !errors.Is(err, imageDomain.ErrImageNotFound) {
|
||||
t.Errorf("expected image.ErrImageNotFound, got %v", err)
|
||||
}
|
||||
@@ -410,10 +410,10 @@ func TestDeleteImageRequiresExistence(t *testing.T) {
|
||||
|
||||
func TestCreateUserAssignsVerificationCode(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder)
|
||||
|
||||
before := time.Now()
|
||||
result, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
result, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "alice",
|
||||
Password: "hunter2example",
|
||||
Email: "alice@example.com",
|
||||
@@ -441,7 +441,7 @@ func TestCreateUserAssignsVerificationCode(t *testing.T) {
|
||||
t.Errorf("expected verification_expiration about 2 hours from now, got %v", result.VerificationExpiration)
|
||||
}
|
||||
|
||||
second, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
second, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "bob",
|
||||
Password: "hunter2example",
|
||||
Email: "bob@example.com",
|
||||
@@ -457,9 +457,9 @@ func TestCreateUserAssignsVerificationCode(t *testing.T) {
|
||||
func TestCreateUserPropagatesRepositoryFailure(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
repo.fail.failNext("Create")
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder)
|
||||
|
||||
_, err := uc.CreateUser(useCases.CreateUserCommand{
|
||||
_, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{
|
||||
Username: "alice",
|
||||
Password: "hunter2example",
|
||||
Email: "alice@example.com",
|
||||
|
||||
@@ -25,12 +25,12 @@ var displayNameResponseKeys = []string{"id", "username", "email", "is_verified",
|
||||
func newDisplayNameRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
getUser := useCases.NewGetUserUseCase(userRepo)
|
||||
update := useCases.NewUpdateUserProfileUseCase(userRepo)
|
||||
update := useCases.NewUpdateUserProfileUseCase(userRepo, discardRecorder)
|
||||
handler := userhandler.NewHandler(update)
|
||||
meHandler := auth.NewMeHandler(getUser)
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
@@ -149,7 +149,7 @@ func TestUpdateUserProfileUseCase(t *testing.T) {
|
||||
for _, tc := range successCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
got, err := useCases.NewUpdateUserProfileUseCase(repo).UpdateDisplayName(7, tc.input)
|
||||
got, err := useCases.NewUpdateUserProfileUseCase(repo, discardRecorder).UpdateDisplayName(testCtx, 7, tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func TestUpdateUserProfileUseCase(t *testing.T) {
|
||||
|
||||
t.Run("61 emoji runes are refused without calling the repository", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
_, err := useCases.NewUpdateUserProfileUseCase(repo).UpdateDisplayName(7, strings.Repeat("😀", 61))
|
||||
_, err := useCases.NewUpdateUserProfileUseCase(repo, discardRecorder).UpdateDisplayName(testCtx, 7, strings.Repeat("😀", 61))
|
||||
if !errors.Is(err, user.ErrInvalidDisplayName) {
|
||||
t.Fatalf("expected ErrInvalidDisplayName, got %v", err)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func TestUpdateUserProfileUseCase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("unknown user", func(t *testing.T) {
|
||||
_, err := useCases.NewUpdateUserProfileUseCase(newRepo()).UpdateDisplayName(99, "Marta")
|
||||
_, err := useCases.NewUpdateUserProfileUseCase(newRepo(), discardRecorder).UpdateDisplayName(testCtx, 99, "Marta")
|
||||
if !errors.Is(err, user.ErrUserNotFound) {
|
||||
t.Fatalf("expected ErrUserNotFound, got %v", err)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func TestUpdateUserProfileUseCase(t *testing.T) {
|
||||
t.Run("repository failure is returned unchanged", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
repo.fail.failNext("UpdateDisplayName")
|
||||
_, err := useCases.NewUpdateUserProfileUseCase(repo).UpdateDisplayName(7, "Marta")
|
||||
_, err := useCases.NewUpdateUserProfileUseCase(repo, discardRecorder).UpdateDisplayName(testCtx, 7, "Marta")
|
||||
if !errors.Is(err, errRepositoryFailure) {
|
||||
t.Fatalf("expected repository failure, got %v", err)
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ func newUserFieldHygieneRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, t
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher),
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder),
|
||||
useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -50,19 +50,19 @@ func newUserRolesFixture(t *testing.T, maxAttempts int) *userRolesFixture {
|
||||
getUser := useCases.NewGetUserUseCase(f.users)
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
f.token,
|
||||
useCases.NewCreateUserUseCase(f.users, hasher, f.mail, invitePublicURL),
|
||||
useCases.NewCreateUserUseCase(f.users, hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(f.users, hasher, discardRecorder),
|
||||
true,
|
||||
)
|
||||
acceptHandler := auth.NewAcceptInviteHandler(
|
||||
useCases.NewGetInviteUseCase(f.invites, hasher),
|
||||
useCases.NewAcceptInviteUseCase(f.users, f.invites, hasher),
|
||||
useCases.NewAcceptInviteUseCase(f.users, f.invites, hasher, discardRecorder),
|
||||
f.token,
|
||||
true,
|
||||
)
|
||||
meHandler := auth.NewMeHandler(getUser)
|
||||
inviteHandler := userhandler.NewInviteHandler(
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, hasher, f.mail, invitePublicURL),
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
nil,
|
||||
nil,
|
||||
useCases.NewListUsersAndInvitesUseCase(f.users, f.invites),
|
||||
@@ -352,9 +352,9 @@ func TestUserRolesHttp(t *testing.T) {
|
||||
func TestUserRolesUseCases(t *testing.T) {
|
||||
t.Run("CreateUser returns an owner", func(t *testing.T) {
|
||||
users := newMockUserRepo()
|
||||
uc := useCases.NewCreateUserUseCase(users, &stubHasher{}, newMockMailRepo(), invitePublicURL)
|
||||
uc := useCases.NewCreateUserUseCase(users, &stubHasher{}, newMockMailRepo(), invitePublicURL, discardRecorder)
|
||||
|
||||
created, err := uc.CreateUser(useCases.CreateUserCommand{Username: "marta", Password: "hunter2example12", Email: "marta@renew.example"})
|
||||
created, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{Username: "marta", Password: "hunter2example12", Email: "marta@renew.example"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -371,9 +371,9 @@ func TestUserRolesUseCases(t *testing.T) {
|
||||
pending := pendingInvite(acceptEmail)
|
||||
pending.HashedCode = "hashed:" + acceptCode
|
||||
invites.seed(pending)
|
||||
uc := useCases.NewAcceptInviteUseCase(users, invites, &stubHasher{})
|
||||
uc := useCases.NewAcceptInviteUseCase(users, invites, &stubHasher{}, discardRecorder)
|
||||
|
||||
created, err := uc.AcceptInvite(useCases.AcceptInviteCommand{Code: acceptCode, Username: "lea", Password: acceptPassword})
|
||||
created, err := uc.AcceptInvite(testCtx, useCases.AcceptInviteCommand{Code: acceptCode, Username: "lea", Password: acceptPassword})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
|
||||
+2
-2
@@ -231,7 +231,7 @@ var _ user.Repository = &mockUserRepo{}
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com")
|
||||
uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder)
|
||||
|
||||
cmd := useCases.CreateUserCommand{
|
||||
Username: "testuser",
|
||||
@@ -239,7 +239,7 @@ func TestCreateUser(t *testing.T) {
|
||||
Email: "test@example.com",
|
||||
}
|
||||
|
||||
res, err := uc.CreateUser(cmd)
|
||||
res, err := uc.CreateUser(testCtx, cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("User creation failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
func newVerifiedAccountRequiredRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -213,11 +213,11 @@ func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user