Files
RenewCMS/test/image_variants_env_test.go
T

244 lines
9.5 KiB
Go

package test
import (
"RenewCMS/api"
"RenewCMS/api/handlers/staticimages"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/apikey"
domain "RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/config"
"RenewCMS/internal/infrastructure/hashing"
"RenewCMS/internal/infrastructure/usage"
"RenewCMS/internal/infrastructure/useCases"
"bytes"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/jwtauth/v5"
)
const (
variantOwnerId uint32 = 1
variantAdminId uint32 = 2
variantPlainUserId uint32 = 3
variantUnverifiedId uint32 = 4
variantStaleId uint32 = 5
variantDefaultCap int64 = 10485760
variantDefaultPixels int64 = 50000000
variantDefaultRenders = 4
variantSourceName = "a1b2.png"
)
type variantEnv struct {
t *testing.T
users *mockUserRepo
images *mockImageRepo
variants *mockVariantRepo
files *mockVariantFileRepo
storage *mockVariantStorage
renderer *countingRenderer
audit *activityHarness
articles *mockArticleRepo
keys *mockAPIKeyRepo
logs *bytes.Buffer
token *jwtauth.JWTAuth
now time.Time
usage *mockVariantUsageRepo
tracker *usage.Tracker
admin http.Handler
public http.Handler
}
func newVariantEnv(t *testing.T) *variantEnv {
t.Helper()
return newVariantEnvWith(t, newCountingRenderer(), variantDefaultCap)
}
func newVariantEnvWith(t *testing.T, renderer *countingRenderer, maxSourceBytes int64) *variantEnv {
t.Helper()
return newVariantEnvLimits(t, renderer, useCases.ServeVariantLimits{MaxSourceBytes: maxSourceBytes, MaxSourcePixels: variantDefaultPixels, MaxConcurrentRenders: variantDefaultRenders})
}
func newVariantEnvLimits(t *testing.T, renderer *countingRenderer, limits useCases.ServeVariantLimits) *variantEnv {
t.Helper()
validator.InitValidator()
env := &variantEnv{
t: t,
users: newMockUserRepo(),
images: newMockImageRepo(),
variants: newMockVariantRepo(),
files: newMockVariantFileRepo(),
storage: newMockVariantStorage(),
renderer: renderer,
audit: newActivityHarness(t),
articles: newMockArticleRepo(),
keys: newMockAPIKeyRepo(),
logs: &bytes.Buffer{},
token: jwtauth.New("HS256", []byte("test-secret"), nil),
now: time.Date(2026, time.October, 8, 9, 0, 0, 0, time.UTC),
usage: newMockVariantUsageRepo(),
}
env.tracker = usage.NewTracker(env.usage, func() time.Time { return env.now })
env.users.users[variantOwnerId] = user.User{ID: variantOwnerId, Username: "marta", DisplayName: "Marta Alves", IsVerified: true, Role: user.RoleOwner}
env.users.users[variantAdminId] = user.User{ID: variantAdminId, Username: "dev", IsVerified: true, Role: user.RoleAdmin}
env.users.users[variantPlainUserId] = user.User{ID: variantPlainUserId, Username: "pat", IsVerified: true}
env.users.users[variantUnverifiedId] = user.User{ID: variantUnverifiedId, Username: "quinn", IsVerified: false, Role: user.RoleAdmin}
env.users.users[variantStaleId] = user.User{ID: variantStaleId, Username: "stale", IsVerified: true, Role: user.RoleAdmin, TokenVersion: 1}
container := env.container(limits)
admin := chi.NewRouter()
admin.Mount("/v1", api.InitBackendRoutes(container, config.Settings{}))
env.admin = admin
public := chi.NewRouter()
public.Use(chimiddleware.GetHead)
public.Mount("/static/uploadedImages", staticimages.NewHandler(container.OpenMediaUseCase, container.ServeVariantUseCase))
env.public = public
return env
}
func (e *variantEnv) container(limits useCases.ServeVariantLimits) *api.UseCases {
clock := func() time.Time { return e.now }
recorder := e.audit.recorder
serve := useCases.NewServeVariantUseCase(e.variants, e.files, e.images, e.storage, e.renderer, limits).WithUsageTracker(e.tracker)
return &api.UseCases{
Token: e.token,
GetUserUseCase: useCases.NewGetUserUseCase(e.users),
OpenMediaUseCase: useCases.NewOpenMediaUseCase(e.storage),
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(e.images, e.storage, e.files, recorder, plainSites()),
MergeMediaUseCase: useCases.NewMergeMediaUseCase(e.images, e.articles, e.storage, e.files, recorder, plainSites()),
ListVariantsUseCase: useCases.NewListVariantsUseCase(e.variants, e.files),
CreateVariantUseCase: useCases.NewCreateVariantUseCase(e.variants, e.renderer, clock, recorder),
UpdateVariantUseCase: useCases.NewUpdateVariantUseCase(e.variants, e.files, e.storage, e.renderer, clock, recorder),
SetVariantEnabledUseCase: useCases.NewSetVariantEnabledUseCase(e.variants, e.files, clock, recorder),
DeleteVariantUseCase: useCases.NewDeleteVariantUseCase(e.variants, e.files, e.storage, recorder),
PurgeVariantUseCase: useCases.NewPurgeVariantUseCase(e.variants, e.files, e.storage, recorder),
ServeVariantUseCase: serve,
GetVariantCacheStatsUseCase: useCases.NewGetVariantCacheStatsUseCase(e.variants, e.files, e.files, e.images, e.usage, e.tracker, clock),
PurgeVariantCacheUseCase: useCases.NewPurgeVariantCacheUseCase(e.variants, e.images, e.files, e.files, e.storage, clock, recorder),
VariantUsageTracker: e.tracker,
CreateImageUseCase: useCases.NewCreateImageUseCase(e.images, e.storage, recorder),
ListMediaUseCase: useCases.NewListMediaUseCase(e.images, e.articles, e.users, e.files, plainSites()),
RenameMediaUseCase: useCases.NewRenameMediaUseCase(e.images, recorder),
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(e.images, e.articles),
ListImageVariantsUseCase: useCases.NewListImageVariantsUseCase(e.images, e.variants, e.files, e.renderer),
GenerateImageVariantUseCase: useCases.NewGenerateImageVariantUseCase(e.images, e.variants, e.files, e.renderer, serve),
GenerateAllImageVariantsUseCase: useCases.NewGenerateAllImageVariantsUseCase(e.images, e.variants, e.files, e.renderer, serve, recorder),
PurgeImageVariantUseCase: useCases.NewPurgeImageVariantUseCase(e.images, e.variants, e.files, e.storage, recorder),
GenerateVariantsOnUploadUseCase: useCases.NewGenerateVariantsOnUploadUseCase(e.variants, serve, log.New(e.logs, "", 0)),
AuthenticateAPIKeyUseCase: useCases.NewAuthenticateAPIKeyUseCase(e.keys, hashing.NewSHA256KeyGenerator(), e.users, clock),
ListActivityUseCase: useCases.NewListActivityUseCase(e.audit.probe, e.users, e.articles, e.images, e.keys, clock, plainSites()),
}
}
func (e *variantEnv) issueAPIKey() string {
generator := hashing.NewSHA256KeyGenerator()
raw, prefix, hashed := generator.Generate()
e.keys.Create(apikey.APIKey{UserID: variantOwnerId, Name: "ci", Prefix: prefix, HashedKey: hashed, CreatedAt: e.now})
return raw
}
func (e *variantEnv) cookie(userId uint32, tokenVersion uint32) *http.Cookie {
e.t.Helper()
_, signed, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
if err != nil {
e.t.Fatalf("failed to encode jwt: %v", err)
}
return &http.Cookie{Name: "jwt", Value: signed}
}
func (e *variantEnv) send(method string, path string, body string, userId uint32) *httptest.ResponseRecorder {
e.t.Helper()
request := httptest.NewRequest(method, path, strings.NewReader(body))
if body != "" {
request.Header.Set("Content-Type", "application/json")
}
if userId != 0 {
request.AddCookie(e.cookie(userId, 0))
}
recorder := httptest.NewRecorder()
e.admin.ServeHTTP(recorder, request)
return recorder
}
func (e *variantEnv) sendAsAdmin(method string, path string, body string) *httptest.ResponseRecorder {
e.t.Helper()
return e.send(method, path, body, variantAdminId)
}
func (e *variantEnv) get(path string, header http.Header) *httptest.ResponseRecorder {
e.t.Helper()
request := httptest.NewRequest(http.MethodGet, path, nil)
for name, values := range header {
request.Header[name] = values
}
recorder := httptest.NewRecorder()
e.public.ServeHTTP(recorder, request)
return recorder
}
func (e *variantEnv) seedSource(storedName string, content []byte) {
e.storage.seed(storedName, content)
if known, _ := e.images.ExistsByStoredName(storedName); !known {
e.seedImage(e.images.nextID+1, storedName)
}
}
func (e *variantEnv) seedImage(id uint32, storedName string) domain.Image {
image := domain.Image{ID: id, Path: domain.PublicPath(storedName), Filename: storedName, ContentType: "image/png", SHA256: "same"}
e.images.images[id] = image
if id > e.images.nextID {
e.images.nextID = id
}
return image
}
func (e *variantEnv) seedThumb() domain.Variant {
return seedVariant(e.variants, "thumb", 400, 300, domain.FitCover, domain.FormatJPEG, 80)
}
func (e *variantEnv) seedPhoto() {
e.seedSource(variantSourceName, encodePNG(e.t, variantSolidImage(1000, 800, variantRed)))
}
func (e *variantEnv) events() int {
e.t.Helper()
return len(e.audit.all(e.t))
}
func (e *variantEnv) lastEvent() (string, map[string]any) {
e.t.Helper()
events := e.audit.all(e.t)
if len(events) == 0 {
e.t.Fatal("no event recorded")
}
encoded, _ := json.Marshal(events[0])
var decoded map[string]any
_ = json.Unmarshal(encoded, &decoded)
return events[0].Action, decoded
}
func decodeBody(t *testing.T, recorder *httptest.ResponseRecorder) map[string]any {
t.Helper()
var decoded map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &decoded); err != nil {
t.Fatalf("body is not a json object: %q", recorder.Body.String())
}
return decoded
}
func bearerKeyHeader() http.Header {
return http.Header{"Authorization": []string{"Bearer " + apikey.RawKeyPrefix + "validkey"}}
}