A fetch must send the ID of the request it serves as X-Request-Id, and the "upstream fetched", "image converted" and "image served" lines must carry it as request_id, through either image route. newSignedHostServer takes the logger its handlers and image service write to; its existing callers pass a discarding one. Model: opus-5-5
132 lines
3.1 KiB
Go
132 lines
3.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/imgcache"
|
|
)
|
|
|
|
// signedPhotoURL returns a signed /v1/image/ URL, valid for a minute, for the
|
|
// JPEG at photoPath on signedHost at 50x50, made with h's image service.
|
|
func signedPhotoURL(t *testing.T, h *Handlers) string {
|
|
t.Helper()
|
|
|
|
signedURL, err := h.imgSvc.GenerateSignedURL("", &imgcache.ImageRequest{
|
|
SourceHost: signedHost,
|
|
SourcePath: photoPath,
|
|
Size: imgcache.Size{Width: 50, Height: 50},
|
|
Format: imgcache.FormatJPEG,
|
|
}, time.Minute)
|
|
if err != nil {
|
|
t.Fatalf("GenerateSignedURL() error = %v", err)
|
|
}
|
|
|
|
return signedURL
|
|
}
|
|
|
|
// encPhotoURL returns an encrypted /v1/e/ URL, which never expires, for the
|
|
// JPEG at photoPath on signedHost at 50x50, made with h's generator.
|
|
func encPhotoURL(t *testing.T, h *Handlers) string {
|
|
t.Helper()
|
|
|
|
token, err := h.encGen.Generate(&encurl.Payload{
|
|
SourceHost: signedHost,
|
|
SourcePath: photoPath,
|
|
Width: 50,
|
|
Height: 50,
|
|
Format: imgcache.FormatJPEG,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
return "/v1/e/" + token + "/img.jpg"
|
|
}
|
|
|
|
// requestIDByMessage reads the JSON log lines in logs and returns the
|
|
// request_id of each line, by its message.
|
|
func requestIDByMessage(t *testing.T, logs io.Reader) map[string]string {
|
|
t.Helper()
|
|
|
|
logged := make(map[string]string)
|
|
|
|
dec := json.NewDecoder(logs)
|
|
for dec.More() {
|
|
var line map[string]any
|
|
|
|
err := dec.Decode(&line)
|
|
if err != nil {
|
|
t.Fatalf("decoding log line: %v", err)
|
|
}
|
|
|
|
msg, _ := line["msg"].(string)
|
|
requestID, _ := line["request_id"].(string)
|
|
logged[msg] = requestID
|
|
}
|
|
|
|
return logged
|
|
}
|
|
|
|
// TestImageLogLinesCarryRequestID verifies that the lines logged when an image
|
|
// is fetched, converted and served through either image route carry the
|
|
// request's ID as request_id, as the request log line does, so they can be
|
|
// found from it.
|
|
func TestImageLogLinesCarryRequestID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const requestID = "test-request-id"
|
|
|
|
imageURLs := map[string]func(*testing.T, *Handlers) string{
|
|
"/v1/image/": signedPhotoURL,
|
|
"/v1/e/": encPhotoURL,
|
|
}
|
|
|
|
for route, imageURL := range imageURLs {
|
|
t.Run(route, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var logs bytes.Buffer
|
|
|
|
h, srv := newSignedHostServer(t,
|
|
slog.New(slog.NewJSONHandler(&logs, nil)))
|
|
|
|
ctx := context.WithValue(t.Context(),
|
|
middleware.RequestIDKey, requestID)
|
|
rec := httptest.NewRecorder()
|
|
srv.ServeHTTP(rec, httptest.NewRequestWithContext(
|
|
ctx, http.MethodGet, imageURL(t, h), nil))
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
|
|
t.Logf("logged:\n%s", logs.String())
|
|
|
|
logged := requestIDByMessage(t, &logs)
|
|
|
|
for _, msg := range []string{
|
|
"upstream fetched", "image converted", "image served",
|
|
} {
|
|
got, ok := logged[msg]
|
|
if !ok {
|
|
t.Errorf("no %q line logged", msg)
|
|
} else if got != requestID {
|
|
t.Errorf("%q line has request_id %q, want %q",
|
|
msg, got, requestID)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|