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/useCases" "encoding/json" "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 token *jwtauth.JWTAuth now time.Time 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), token: jwtauth.New("HS256", []byte("test-secret"), nil), now: time.Date(2026, time.October, 8, 9, 0, 0, 0, time.UTC), } 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 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), MergeMediaUseCase: useCases.NewMergeMediaUseCase(e.images, newMockArticleRepo(), e.storage, e.files, recorder), 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: useCases.NewServeVariantUseCase(e.variants, e.files, e.images, e.storage, e.renderer, limits), } } 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"}} }