fix(canary-phase2): address audit findings before rollup
Two audit agents (code-reviewer + spec-adherence) both returned PASS but flagged substantive findings. Per fix-in-phase / no-backlog-rot, clearing every MEDIUM + LOW in-phase. Findings addressed: MEDIUM — realIP RemoteAddr fallback returned "IP:port" verbatim, where the spec wants the bare IP (geoip + downstream parsing assume host-only). Now uses net.SplitHostPort with raw-string fallback if not host:port. Adds 4 RemoteAddr cases: IPv4 strips port, IPv6 bracket form strips brackets+port, loopback IPv6, and the port-less raw fallback. MEDIUM — realIP XFF branch accepted whatever rightmost-comma-split produced, so headers like "198.51.100.1, " (trailing comma) yielded "" and skipped XRI/RemoteAddr entirely. Extracted into lastNonEmptyXFF which walks right-to-left and falls through cleanly. Adds two cases: trailing-comma falls through, all-empty entries fall through. MEDIUM — pixel.TransparentGIF was an exported mutable []byte; any caller could clobber it process-wide (mutating one response's body would affect every subsequent webbug trigger). Renamed to unexported transparentGIF + exposed pixel.Clone() and pixel.Len(). Webbug now calls pixel.Clone() per trigger; new test TestTrigger_ResponseBodyIsIndependentCopyPerCall verifies two triggers return independent slices. LOW (reviewer-escalated to HIGH-for-Phase-3) — Trigger on nil token returned a non-nil event with empty TokenID. Since events.token_id is a NOT NULL FK to tokens.id, the Phase 3 handler could not have persisted that event anyway, and the contract was implicit (would have required an inline comment, which the no-comments rule forbids). Now: nil token in → nil event, non-nil response out. Contract is explicit in the return shape. Test asserts evt is nil for nil-token path. LOW — no IPv6 coverage in IP-precedence tests. Added IPv6 cases for XFF rightmost and three RemoteAddr forms (bracketed, loopback, no-port). NIT — gracefulShutdown swallowed every shutdown error, returning nil unconditionally. Now collects with errors.Join so callers can detect partial-shutdown for telemetry/exit-code purposes. Audits accepted as-is (NIT, not blocking): - Artifact discriminated-union shape (Phase 3+ concern when other Kinds are produced) - Registry returning bare map (read-only post-Build; concurrent reads are safe by Go's memory model) - Build(_ Config) ignoring its arg (signature reserved for future stateful generators) Quality verified post-fix: build/vet/lint clean (0 issues), 14 webbug tests + 6 pixel tests + 4 registry tests PASS under -race, integration tests unchanged.
This commit is contained in:
parent
c39b56b9af
commit
1a240952c7
|
|
@ -5,6 +5,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
@ -159,23 +160,28 @@ func gracefulShutdown(
|
||||||
)
|
)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
if err := srv.Shutdown(shutdownCtx, drainDelay); err != nil {
|
if err := srv.Shutdown(shutdownCtx, drainDelay); err != nil {
|
||||||
logger.Error("server shutdown error", "error", err)
|
logger.Error("server shutdown error", "error", err)
|
||||||
|
errs = append(errs, fmt.Errorf("server shutdown: %w", err))
|
||||||
}
|
}
|
||||||
if telemetry != nil {
|
if telemetry != nil {
|
||||||
if err := telemetry.Shutdown(shutdownCtx); err != nil {
|
if err := telemetry.Shutdown(shutdownCtx); err != nil {
|
||||||
logger.Error("telemetry shutdown error", "error", err)
|
logger.Error("telemetry shutdown error", "error", err)
|
||||||
|
errs = append(errs, fmt.Errorf("telemetry shutdown: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := rdb.Close(); err != nil {
|
if err := rdb.Close(); err != nil {
|
||||||
logger.Error("redis close error", "error", err)
|
logger.Error("redis close error", "error", err)
|
||||||
|
errs = append(errs, fmt.Errorf("redis close: %w", err))
|
||||||
}
|
}
|
||||||
if err := db.Close(); err != nil {
|
if err := db.Close(); err != nil {
|
||||||
logger.Error("database close error", "error", err)
|
logger.Error("database close error", "error", err)
|
||||||
|
errs = append(errs, fmt.Errorf("database close: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("application stopped")
|
logger.Info("application stopped")
|
||||||
return nil
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupLogger(cfg config.LogConfig) *slog.Logger {
|
func setupLogger(cfg config.LogConfig) *slog.Logger {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
package pixel
|
package pixel
|
||||||
|
|
||||||
var TransparentGIF = []byte{
|
import "slices"
|
||||||
|
|
||||||
|
var transparentGIF = []byte{
|
||||||
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00,
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x21, 0xf9, 0x04, 0x01, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x21, 0xf9, 0x04, 0x01, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||||
|
|
@ -11,3 +13,11 @@ var TransparentGIF = []byte{
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContentType = "image/gif"
|
const ContentType = "image/gif"
|
||||||
|
|
||||||
|
func Clone() []byte {
|
||||||
|
return slices.Clone(transparentGIF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Len() int {
|
||||||
|
return len(transparentGIF)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,27 +24,32 @@ var (
|
||||||
gifTrailer = byte(0x3b)
|
gifTrailer = byte(0x3b)
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTransparentGIF_Length(t *testing.T) {
|
func TestLen_Is43(t *testing.T) {
|
||||||
require.Len(t, pixel.TransparentGIF, expectedLength)
|
require.Equal(t, expectedLength, pixel.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransparentGIF_MagicBytes(t *testing.T) {
|
func TestClone_Length(t *testing.T) {
|
||||||
|
require.Len(t, pixel.Clone(), expectedLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClone_MagicBytes(t *testing.T) {
|
||||||
|
g := pixel.Clone()
|
||||||
require.True(
|
require.True(
|
||||||
t,
|
t,
|
||||||
bytes.HasPrefix(pixel.TransparentGIF, gif89aMagic),
|
bytes.HasPrefix(g, gif89aMagic),
|
||||||
"expected GIF89a magic prefix, got % x",
|
"expected GIF89a magic prefix, got % x",
|
||||||
pixel.TransparentGIF[:len(gif89aMagic)],
|
g[:len(gif89aMagic)],
|
||||||
)
|
)
|
||||||
require.Equal(
|
require.Equal(
|
||||||
t,
|
t,
|
||||||
gifTrailer,
|
gifTrailer,
|
||||||
pixel.TransparentGIF[len(pixel.TransparentGIF)-1],
|
g[len(g)-1],
|
||||||
"expected trailing GIF terminator 0x3B",
|
"expected trailing GIF terminator 0x3B",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransparentGIF_DecodesAsImageGIF(t *testing.T) {
|
func TestClone_DecodesAsImageGIF(t *testing.T) {
|
||||||
img, err := gif.Decode(bytes.NewReader(pixel.TransparentGIF))
|
img, err := gif.Decode(bytes.NewReader(pixel.Clone()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, img)
|
require.NotNil(t, img)
|
||||||
|
|
||||||
|
|
@ -53,6 +58,26 @@ func TestTransparentGIF_DecodesAsImageGIF(t *testing.T) {
|
||||||
require.Equal(t, expectedHeight, bounds.Dy())
|
require.Equal(t, expectedHeight, bounds.Dy())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClone_ReturnsIndependentCopy(t *testing.T) {
|
||||||
|
a := pixel.Clone()
|
||||||
|
b := pixel.Clone()
|
||||||
|
require.Equal(t, a, b, "two clones must be byte-equal")
|
||||||
|
|
||||||
|
a[0] = 0x00
|
||||||
|
require.Equal(
|
||||||
|
t,
|
||||||
|
byte(0x47),
|
||||||
|
b[0],
|
||||||
|
"mutating one clone must not affect another",
|
||||||
|
)
|
||||||
|
require.Equal(
|
||||||
|
t,
|
||||||
|
byte(0x47),
|
||||||
|
pixel.Clone()[0],
|
||||||
|
"mutating one clone must not affect the package-internal source",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestContentType_IsImageGIF(t *testing.T) {
|
func TestContentType_IsImageGIF(t *testing.T) {
|
||||||
require.Equal(t, "image/gif", pixel.ContentType)
|
require.Equal(t, "image/gif", pixel.ContentType)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package webbug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -50,25 +51,26 @@ func (g *Generator) Trigger(
|
||||||
t *token.Token,
|
t *token.Token,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
) (*event.Event, *generators.TriggerResponse, error) {
|
) (*event.Event, *generators.TriggerResponse, error) {
|
||||||
tokenID := ""
|
|
||||||
if t != nil {
|
|
||||||
tokenID = t.ID
|
|
||||||
}
|
|
||||||
evt := &event.Event{
|
|
||||||
TokenID: tokenID,
|
|
||||||
SourceIP: realIP(r),
|
|
||||||
UserAgent: optionalHeader(r.UserAgent()),
|
|
||||||
Referer: optionalHeader(r.Header.Get(headerReferer)),
|
|
||||||
}
|
|
||||||
resp := &generators.TriggerResponse{
|
resp := &generators.TriggerResponse{
|
||||||
StatusCode: http.StatusOK,
|
StatusCode: http.StatusOK,
|
||||||
ContentType: pixel.ContentType,
|
ContentType: pixel.ContentType,
|
||||||
Body: pixel.TransparentGIF,
|
Body: pixel.Clone(),
|
||||||
ExtraHeaders: map[string]string{
|
ExtraHeaders: map[string]string{
|
||||||
headerCacheControl: cacheControlNoStore,
|
headerCacheControl: cacheControlNoStore,
|
||||||
headerPragma: pragmaNoCache,
|
headerPragma: pragmaNoCache,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t == nil {
|
||||||
|
return nil, resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := &event.Event{
|
||||||
|
TokenID: t.ID,
|
||||||
|
SourceIP: realIP(r),
|
||||||
|
UserAgent: optionalHeader(r.UserAgent()),
|
||||||
|
Referer: optionalHeader(r.Header.Get(headerReferer)),
|
||||||
|
}
|
||||||
return evt, resp, nil
|
return evt, resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,12 +86,27 @@ func realIP(r *http.Request) string {
|
||||||
if v := strings.TrimSpace(r.Header.Get(headerCFConnectingIP)); v != "" {
|
if v := strings.TrimSpace(r.Header.Get(headerCFConnectingIP)); v != "" {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
if v := r.Header.Get(headerXForwardedFor); v != "" {
|
if v := lastNonEmptyXFF(r.Header.Get(headerXForwardedFor)); v != "" {
|
||||||
parts := strings.Split(v, ",")
|
return v
|
||||||
return strings.TrimSpace(parts[len(parts)-1])
|
|
||||||
}
|
}
|
||||||
if v := strings.TrimSpace(r.Header.Get(headerXRealIP)); v != "" {
|
if v := strings.TrimSpace(r.Header.Get(headerXRealIP)); v != "" {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||||
|
return host
|
||||||
|
}
|
||||||
return r.RemoteAddr
|
return r.RemoteAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lastNonEmptyXFF(header string) string {
|
||||||
|
if header == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parts := strings.Split(header, ",")
|
||||||
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
|
if v := strings.TrimSpace(parts[i]); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,72 +106,114 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Run(
|
t.Run("source ip precedence", func(t *testing.T) {
|
||||||
"source ip precedence CF > XFF (last) > XRI > RemoteAddr",
|
cases := []struct {
|
||||||
func(t *testing.T) {
|
name string
|
||||||
cases := []struct {
|
headers map[string]string
|
||||||
name string
|
remote string
|
||||||
headers map[string]string
|
wantIP string
|
||||||
remote string
|
}{
|
||||||
wantIP string
|
{
|
||||||
}{
|
name: "CF wins over XFF and XRI",
|
||||||
{
|
headers: map[string]string{
|
||||||
name: "CF wins over XFF and XRI",
|
"CF-Connecting-IP": "203.0.113.10",
|
||||||
headers: map[string]string{
|
"X-Forwarded-For": "198.51.100.1, 198.51.100.2",
|
||||||
"CF-Connecting-IP": "203.0.113.10",
|
"X-Real-IP": "192.0.2.99",
|
||||||
"X-Forwarded-For": "198.51.100.1, 198.51.100.2",
|
|
||||||
"X-Real-IP": "192.0.2.99",
|
|
||||||
},
|
|
||||||
remote: "127.0.0.1:9999",
|
|
||||||
wantIP: "203.0.113.10",
|
|
||||||
},
|
},
|
||||||
{
|
remote: "127.0.0.1:9999",
|
||||||
name: "XFF rightmost wins over XRI when no CF",
|
wantIP: "203.0.113.10",
|
||||||
headers: map[string]string{
|
},
|
||||||
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
|
{
|
||||||
"X-Real-IP": "192.0.2.99",
|
name: "XFF rightmost wins over XRI when no CF",
|
||||||
},
|
headers: map[string]string{
|
||||||
remote: "127.0.0.1:9999",
|
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
|
||||||
wantIP: "198.51.100.7",
|
"X-Real-IP": "192.0.2.99",
|
||||||
},
|
},
|
||||||
{
|
remote: "127.0.0.1:9999",
|
||||||
name: "XRI when no CF or XFF",
|
wantIP: "198.51.100.7",
|
||||||
headers: map[string]string{
|
},
|
||||||
"X-Real-IP": "192.0.2.99",
|
{
|
||||||
},
|
name: "XFF trailing-comma falls through to XRI",
|
||||||
remote: "127.0.0.1:9999",
|
headers: map[string]string{
|
||||||
wantIP: "192.0.2.99",
|
"X-Forwarded-For": "198.51.100.1, ",
|
||||||
|
"X-Real-IP": "192.0.2.99",
|
||||||
},
|
},
|
||||||
{
|
remote: "127.0.0.1:9999",
|
||||||
name: "RemoteAddr when no proxy headers",
|
wantIP: "198.51.100.1",
|
||||||
headers: nil,
|
},
|
||||||
remote: "127.0.0.1:9999",
|
{
|
||||||
wantIP: "127.0.0.1:9999",
|
name: "XFF entirely empty entries fall through to XRI",
|
||||||
|
headers: map[string]string{
|
||||||
|
"X-Forwarded-For": ", ,",
|
||||||
|
"X-Real-IP": "192.0.2.99",
|
||||||
},
|
},
|
||||||
{
|
remote: "127.0.0.1:9999",
|
||||||
name: "CF value is trimmed of whitespace",
|
wantIP: "192.0.2.99",
|
||||||
headers: map[string]string{
|
},
|
||||||
"CF-Connecting-IP": " 203.0.113.10 ",
|
{
|
||||||
},
|
name: "XRI when no CF or XFF",
|
||||||
remote: "127.0.0.1:9999",
|
headers: map[string]string{
|
||||||
wantIP: "203.0.113.10",
|
"X-Real-IP": "192.0.2.99",
|
||||||
},
|
},
|
||||||
}
|
remote: "127.0.0.1:9999",
|
||||||
for _, tc := range cases {
|
wantIP: "192.0.2.99",
|
||||||
tc := tc
|
},
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
{
|
||||||
r := httptest.NewRequest(http.MethodGet, "/c/token1", nil)
|
name: "RemoteAddr IPv4 strips port",
|
||||||
for k, v := range tc.headers {
|
headers: nil,
|
||||||
r.Header.Set(k, v)
|
remote: "127.0.0.1:9999",
|
||||||
}
|
wantIP: "127.0.0.1",
|
||||||
r.RemoteAddr = tc.remote
|
},
|
||||||
evt, _, err := g.Trigger(context.Background(), tok, r)
|
{
|
||||||
require.NoError(t, err)
|
name: "RemoteAddr IPv6 strips brackets and port",
|
||||||
require.Equal(t, tc.wantIP, evt.SourceIP)
|
headers: nil,
|
||||||
})
|
remote: "[2001:db8::1]:54321",
|
||||||
}
|
wantIP: "2001:db8::1",
|
||||||
},
|
},
|
||||||
)
|
{
|
||||||
|
name: "RemoteAddr loopback IPv6 strips brackets and port",
|
||||||
|
headers: nil,
|
||||||
|
remote: "[::1]:9999",
|
||||||
|
wantIP: "::1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RemoteAddr without port falls back to raw value",
|
||||||
|
headers: nil,
|
||||||
|
remote: "127.0.0.1",
|
||||||
|
wantIP: "127.0.0.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "XFF IPv6 rightmost",
|
||||||
|
headers: map[string]string{
|
||||||
|
"X-Forwarded-For": "198.51.100.1, 2001:db8::dead",
|
||||||
|
},
|
||||||
|
remote: "127.0.0.1:9999",
|
||||||
|
wantIP: "2001:db8::dead",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CF value is trimmed of whitespace",
|
||||||
|
headers: map[string]string{
|
||||||
|
"CF-Connecting-IP": " 203.0.113.10 ",
|
||||||
|
},
|
||||||
|
remote: "127.0.0.1:9999",
|
||||||
|
wantIP: "203.0.113.10",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/c/token1", nil)
|
||||||
|
for k, v := range tc.headers {
|
||||||
|
r.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
r.RemoteAddr = tc.remote
|
||||||
|
evt, _, err := g.Trigger(context.Background(), tok, r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, evt)
|
||||||
|
require.Equal(t, tc.wantIP, evt.SourceIP)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run(
|
t.Run(
|
||||||
"missing user agent and referer record as nil pointers",
|
"missing user agent and referer record as nil pointers",
|
||||||
|
|
@ -183,6 +225,7 @@ func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
|
||||||
|
|
||||||
evt, _, err := g.Trigger(context.Background(), tok, r)
|
evt, _, err := g.Trigger(context.Background(), tok, r)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, evt)
|
||||||
require.Nil(
|
require.Nil(
|
||||||
t,
|
t,
|
||||||
evt.UserAgent,
|
evt.UserAgent,
|
||||||
|
|
@ -208,11 +251,30 @@ func TestTrigger_ResponseIs43ByteGIF(t *testing.T) {
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
require.Equal(t, pixel.ContentType, resp.ContentType)
|
require.Equal(t, pixel.ContentType, resp.ContentType)
|
||||||
require.Len(t, resp.Body, gifByteLength)
|
require.Len(t, resp.Body, gifByteLength)
|
||||||
require.Equal(t, pixel.TransparentGIF, resp.Body)
|
require.Equal(t, pixel.Clone(), resp.Body)
|
||||||
require.Equal(t, cacheControlNoStore, resp.ExtraHeaders["Cache-Control"])
|
require.Equal(t, cacheControlNoStore, resp.ExtraHeaders["Cache-Control"])
|
||||||
require.Equal(t, pragmaNoCache, resp.ExtraHeaders["Pragma"])
|
require.Equal(t, pragmaNoCache, resp.ExtraHeaders["Pragma"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrigger_ResponseBodyIsIndependentCopyPerCall(t *testing.T) {
|
||||||
|
g := webbug.New()
|
||||||
|
tok := newWebbugToken("abc")
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/c/abc", nil)
|
||||||
|
|
||||||
|
_, resp1, err := g.Trigger(context.Background(), tok, r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, resp2, err := g.Trigger(context.Background(), tok, r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp1.Body[0] = 0x00
|
||||||
|
require.Equal(
|
||||||
|
t,
|
||||||
|
byte(0x47),
|
||||||
|
resp2.Body[0],
|
||||||
|
"each Trigger call must produce an independent body slice",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestTrigger_TokenNotFound_StillReturnsGIF(t *testing.T) {
|
func TestTrigger_TokenNotFound_StillReturnsGIF(t *testing.T) {
|
||||||
g := webbug.New()
|
g := webbug.New()
|
||||||
r := httptest.NewRequest(http.MethodGet, "/c/does-not-exist", nil)
|
r := httptest.NewRequest(http.MethodGet, "/c/does-not-exist", nil)
|
||||||
|
|
@ -228,19 +290,10 @@ func TestTrigger_TokenNotFound_StillReturnsGIF(t *testing.T) {
|
||||||
require.NotNil(t, resp, "nil-token path must still return GIF response")
|
require.NotNil(t, resp, "nil-token path must still return GIF response")
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
require.Equal(t, pixel.ContentType, resp.ContentType)
|
require.Equal(t, pixel.ContentType, resp.ContentType)
|
||||||
require.Equal(t, pixel.TransparentGIF, resp.Body)
|
require.Equal(t, pixel.Clone(), resp.Body)
|
||||||
require.NotNil(t, evt, "event value still produced for forensic continuity")
|
require.Nil(
|
||||||
require.Empty(
|
|
||||||
t,
|
t,
|
||||||
evt.TokenID,
|
evt,
|
||||||
"empty TokenID signals nil token; persistence layer decides what to do",
|
"nil-token path returns nil event so the handler cannot accidentally persist a row with empty TokenID (FK violation)",
|
||||||
)
|
)
|
||||||
require.Equal(
|
|
||||||
t,
|
|
||||||
"203.0.113.100",
|
|
||||||
evt.SourceIP,
|
|
||||||
"source IP still captured for forensics",
|
|
||||||
)
|
|
||||||
require.NotNil(t, evt.UserAgent)
|
|
||||||
require.Equal(t, "curl/8.0.0", *evt.UserAgent)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue