fear(client): extract client's IP and store in database and structured logs

This commit is contained in:
2026-09-21 22:47:10 +03:30
parent 8bea266f03
commit faadf94927
4 changed files with 295 additions and 0 deletions

View File

@@ -248,6 +248,10 @@ func TestHandlerConvert(t *testing.T) {
t.Errorf("masked card = %q, want %q", record.MaskedCard, tt.wantMaskedCard)
}
if record.ClientIP == nil || *record.ClientIP != "192.0.2.1" {
t.Errorf("client IP = %v, want 192.0.2.1", record.ClientIP)
}
if strings.Contains(string(record.ResponseJSON), "6037997512345678") {
t.Error("audit response contains the full card number")
}
@@ -291,3 +295,75 @@ func TestHandlerFailsClosedWhenAuditSaveFails(t *testing.T) {
t.Errorf("response body = %s, want internal_error", response.Body.String())
}
}
func TestHandlerExtractsClientIP(t *testing.T) {
tests := []struct {
name string
headerKey string
headerVal string
remoteAddr string
wantIP string
}{
{
name: "prefers CF-Connecting-IP",
headerKey: "CF-Connecting-IP",
headerVal: "203.0.113.88",
remoteAddr: "10.0.0.1:1234",
wantIP: "203.0.113.88",
},
{
name: "prefers X-Forwarded-For over RemoteAddr",
headerKey: "X-Forwarded-For",
headerVal: "198.51.100.77, 10.0.0.2",
remoteAddr: "10.0.0.1:1234",
wantIP: "198.51.100.77",
},
{
name: "uses RemoteAddr host without port",
remoteAddr: "192.168.10.5:9999",
wantIP: "192.168.10.5",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
audits := &fakeAuditSaver{}
handler := NewHandler(
&fakeConverter{
result: service.ConversionResult{
IBAN: "IR820540102680020817909002",
UpstreamHTTPStatus: http.StatusOK,
},
},
audits,
slog.New(slog.NewTextHandler(io.Discard, nil)),
)
req := httptest.NewRequest(
http.MethodPost,
"/api/v1/card-to-iban",
strings.NewReader(`{"card":"6037997512345678"}`),
)
if tt.headerKey != "" {
req.Header.Set(tt.headerKey, tt.headerVal)
}
req.RemoteAddr = tt.remoteAddr
rec := httptest.NewRecorder()
handler.Convert(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
if len(audits.records) != 1 {
t.Fatalf("audit records count = %d, want 1", len(audits.records))
}
recIP := audits.records[0].ClientIP
if recIP == nil || *recIP != tt.wantIP {
t.Errorf("ClientIP = %v, want %q", recIP, tt.wantIP)
}
})
}
}