Test that the upstream fetch and image log lines carry the request ID
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
This commit is contained in:
@@ -23,8 +23,10 @@ const photoPath = "/images/photo.jpg"
|
|||||||
// newSignedHostServer returns a router for both image routes, and the Handlers
|
// newSignedHostServer returns a router for both image routes, and the Handlers
|
||||||
// behind it, whose fetcher serves a JPEG at photoPath on signedHost. signedHost
|
// behind it, whose fetcher serves a JPEG at photoPath on signedHost. signedHost
|
||||||
// is not on the allowlist, so a /v1/image/ URL for it is served only with a
|
// is not on the allowlist, so a /v1/image/ URL for it is served only with a
|
||||||
// valid signature.
|
// valid signature. The handlers and the image service log to log.
|
||||||
func newSignedHostServer(t *testing.T) (*Handlers, http.Handler) {
|
func newSignedHostServer(
|
||||||
|
t *testing.T, log *slog.Logger,
|
||||||
|
) (*Handlers, http.Handler) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cache, err := imgcache.NewCache(setupTestDB(t), imgcache.CacheConfig{
|
cache, err := imgcache.NewCache(setupTestDB(t), imgcache.CacheConfig{
|
||||||
@@ -44,6 +46,7 @@ func newSignedHostServer(t *testing.T) (*Handlers, http.Handler) {
|
|||||||
signedHost + photoPath: &fstest.MapFile{Data: jpegData},
|
signedHost + photoPath: &fstest.MapFile{Data: jpegData},
|
||||||
}),
|
}),
|
||||||
SigningKey: testSigningKey,
|
SigningKey: testSigningKey,
|
||||||
|
Logger: log,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("imgcache.NewService() error = %v", err)
|
t.Fatalf("imgcache.NewService() error = %v", err)
|
||||||
@@ -55,7 +58,7 @@ func newSignedHostServer(t *testing.T) (*Handlers, http.Handler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := &Handlers{
|
h := &Handlers{
|
||||||
log: slog.New(slog.DiscardHandler),
|
log: log,
|
||||||
imgSvc: svc,
|
imgSvc: svc,
|
||||||
encGen: encGen,
|
encGen: encGen,
|
||||||
}
|
}
|
||||||
@@ -103,7 +106,7 @@ func getMaxAge(t *testing.T, srv http.Handler, target string) int {
|
|||||||
func TestHandleImage_SignedURL_MaxAgeEndsAtExp(t *testing.T) {
|
func TestHandleImage_SignedURL_MaxAgeEndsAtExp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
h, srv := newSignedHostServer(t)
|
h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
||||||
|
|
||||||
signedURL, err := h.imgSvc.GenerateSignedURL("", &imgcache.ImageRequest{
|
signedURL, err := h.imgSvc.GenerateSignedURL("", &imgcache.ImageRequest{
|
||||||
SourceHost: signedHost,
|
SourceHost: signedHost,
|
||||||
@@ -179,7 +182,7 @@ func TestHandleImageEnc_MaxAge(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
h, srv := newSignedHostServer(t)
|
h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
||||||
|
|
||||||
token, err := h.encGen.Generate(&encurl.Payload{
|
token, err := h.encGen.Generate(&encurl.Payload{
|
||||||
SourceHost: signedHost,
|
SourceHost: signedHost,
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package httpfetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestFetchSendsRequestID verifies that a fetch sends the ID of the request
|
||||||
|
// it serves, which chi's RequestID middleware stores in the request context,
|
||||||
|
// to the upstream host as X-Request-Id, so the fetch can be found in that
|
||||||
|
// host's logs.
|
||||||
|
func TestFetchSendsRequestID(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const requestID = "test-request-id"
|
||||||
|
|
||||||
|
received := make(chan string, 1)
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
received <- r.Header.Get("X-Request-Id")
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", contentTypeJPEG)
|
||||||
|
_, _ = io.WriteString(w, imagePayload)
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
f, _ := newServerFetcher(t, srv, nil)
|
||||||
|
|
||||||
|
ctx := context.WithValue(testContext(t), middleware.RequestIDKey, requestID)
|
||||||
|
|
||||||
|
res, err := f.Fetch(ctx, upstreamURL("/image"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Fetch() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = res.Content.Close()
|
||||||
|
|
||||||
|
got := <-received
|
||||||
|
t.Logf("upstream received X-Request-Id %q", got)
|
||||||
|
|
||||||
|
if got != requestID {
|
||||||
|
t.Errorf("upstream X-Request-Id = %q, want %q", got, requestID)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user