Build the image pipeline in pixad's fx graph (closes #313)
check / check (push) Canceled after 0s

internal/app now provides the cache, the fetcher, the processor, the
image service, the session manager and the encrypted URL generator, and
handlers.New receives them built. database.New opens the database, so
these parts can read it while fx builds them; the cache's stop hook
still runs before the database's.

Each setting's default is declared in internal/config, the default
cache_max_bytes included, which internal/app works out before it builds
the cache. The cache's MaxBytes is the one value for the disk cache
limit, and 0 turns the disk cache off. The fetcher and the processor
take explicit values. The server tests build the pipeline themselves,
as they cannot import internal/app.

Model: opus-5-5
This commit is contained in:
2026-10-26 10:31:44 +00:00
parent ac2bae0489
commit 27493568fd
54 changed files with 1219 additions and 1064 deletions
@@ -119,7 +119,7 @@ func detectMIME(data []byte) string {
func TestImageProcessor_ResizeJPEG(t *testing.T) {
t.Parallel()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
input := createTestJPEG(t, 800, 600)
@@ -165,7 +165,7 @@ func TestImageProcessor_ResizeJPEG(t *testing.T) {
func TestImageProcessor_ConvertToPNG(t *testing.T) {
t.Parallel()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
input := createTestJPEG(t, 200, 150)
@@ -201,7 +201,7 @@ func processAndCheckSize(
) {
t.Helper()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
input := createTestJPEG(t, inputW, inputH)
@@ -239,7 +239,7 @@ func TestImageProcessor_OriginalSize(t *testing.T) {
func TestImageProcessor_FitContain(t *testing.T) {
t.Parallel()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
// 800x400 image (2:1 aspect) into 400x400 box with contain
@@ -285,7 +285,7 @@ func TestImageProcessor_ProportionalScale_HeightOnly(t *testing.T) {
func TestImageProcessor_ProcessPNG(t *testing.T) {
t.Parallel()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
input := createTestPNG(t, 400, 300)
@@ -330,7 +330,7 @@ func TestImageProcessor_RejectsOversizedInput(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
input := createTestJPEG(t, tt.width, tt.height)
@@ -356,7 +356,7 @@ func TestImageProcessor_RejectsOversizedInput(t *testing.T) {
func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
t.Parallel()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
// Create an image at exactly MaxInputDimension - should be accepted
@@ -387,7 +387,7 @@ func encodeAndCheck(
) {
t.Helper()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
input := createTestJPEG(t, 200, 150)
@@ -436,7 +436,7 @@ func TestImageProcessor_EncodeWebP(t *testing.T) {
func TestImageProcessor_DecodeAVIF(t *testing.T) {
t.Parallel()
proc := New(Params{})
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
ctx := context.Background()
// Load test AVIF file
@@ -478,7 +478,7 @@ func TestImageProcessor_RejectsOversizedInputData(t *testing.T) {
// Create a processor with a very small byte limit
const limit = 1024
proc := New(Params{MaxInputBytes: limit})
proc := New(Params{MaxInputBytes: limit, MaxConcurrentProcessing: 1})
ctx := context.Background()
// Create a valid JPEG that exceeds the byte limit
@@ -511,7 +511,7 @@ func TestImageProcessor_AcceptsInputWithinLimit(t *testing.T) {
input := createTestJPEG(t, 10, 10)
limit := int64(len(input)) * 10 // 10× headroom
proc := New(Params{MaxInputBytes: limit})
proc := New(Params{MaxInputBytes: limit, MaxConcurrentProcessing: 1})
ctx := context.Background()
transform := &imagerequest.Transform{
@@ -529,22 +529,6 @@ func TestImageProcessor_AcceptsInputWithinLimit(t *testing.T) {
defer func() { _ = result.Content.Close() }()
}
func TestImageProcessor_DefaultMaxInputBytes(t *testing.T) {
t.Parallel()
// Passing 0 should use the default
proc := New(Params{})
if proc.maxInputBytes != DefaultMaxInputBytes {
t.Errorf("maxInputBytes = %d, want %d", proc.maxInputBytes, DefaultMaxInputBytes)
}
// Passing negative should also use the default
proc = New(Params{MaxInputBytes: -1})
if proc.maxInputBytes != DefaultMaxInputBytes {
t.Errorf("maxInputBytes = %d, want %d", proc.maxInputBytes, DefaultMaxInputBytes)
}
}
func TestImageProcessor_EncodeAVIF(t *testing.T) {
t.Parallel()
@@ -558,7 +542,10 @@ func processAndDecode(
) *vips.ImageRef {
t.Helper()
result, err := New(Params{}).Process(
result, err := New(Params{
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(
context.Background(), bytes.NewReader(input), transform,
)
if err != nil {