245 lines
9.6 KiB
Go
245 lines
9.6 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),
|
|
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
|
OpenMediaUseCase: useCases.NewOpenMediaUseCase(e.storage),
|
|
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(e.images, e.storage, e.files, recorder, plainSites(), plainAuthors()),
|
|
MergeMediaUseCase: useCases.NewMergeMediaUseCase(e.images, e.articles, e.storage, e.files, recorder, plainSites(), plainAuthors()),
|
|
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(), plainAuthors()),
|
|
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(), plainAuthors()),
|
|
}
|
|
}
|
|
|
|
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"}}
|
|
}
|