package httpfetcher import ( "context" "errors" "fmt" "io/fs" "net/http" "strings" "sneak.berlin/go/pixa/internal/imagerequest" ) // MockFetcher implements Fetcher using an embedded filesystem. // Files are organized as: hostname/path/to/file.ext // The source with host example.com and path /images/photo.jpg maps to // example.com/images/photo.jpg; the source query is ignored. type MockFetcher struct { fs fs.FS } // NewMock creates a new mock fetcher backed by the given filesystem. func NewMock(fsys fs.FS) *MockFetcher { return &MockFetcher{fs: fsys} } // Fetch retrieves the source's content from the mock filesystem. func (m *MockFetcher) Fetch( ctx context.Context, source imagerequest.Source, ) (*FetchResult, error) { // Check context cancellation select { case <-ctx.Done(): return nil, ctx.Err() default: } path := source.Host + source.Path // Open the file f, err := m.fs.Open(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("%w: status 404", ErrUpstreamError) } return nil, fmt.Errorf("failed to open mock file: %w", err) } // Get file info for content length stat, err := f.Stat() if err != nil { _ = f.Close() return nil, fmt.Errorf("failed to stat mock file: %w", err) } // Detect content type from extension contentType := detectContentTypeFromPath(path) return &FetchResult{ Content: f, ContentLength: stat.Size(), ContentType: contentType, Headers: make(http.Header), }, nil } // detectContentTypeFromPath returns the MIME type based on file extension. func detectContentTypeFromPath(path string) string { path = strings.ToLower(path) switch { case strings.HasSuffix(path, ".jpg"), strings.HasSuffix(path, ".jpeg"): return contentTypeJPEG case strings.HasSuffix(path, ".png"): return contentTypePNG case strings.HasSuffix(path, ".gif"): return contentTypeGIF case strings.HasSuffix(path, ".webp"): return contentTypeWebP case strings.HasSuffix(path, ".avif"): return contentTypeAVIF case strings.HasSuffix(path, ".jxl"): return contentTypeJXL case strings.HasSuffix(path, ".svg"): return contentTypeSVG default: return contentTypeOctetStream } }