Files
ddns-updater-qdm12-3/internal/update/logclient_test.go
fredericrous 80bcec2404 perf: skip HTTP body reading when debug logging is disabled
The loggingRoundTripper was unconditionally reading entire HTTP
request/response bodies via io.ReadAll() on every round trip, even
at the default Info log level. The expensive work (body read, string
conversion, concatenation) happened before Debug() was called, so
the logger's internal level check couldn't prevent it.

Now makeLogClient skips wrapping the transport entirely when debug
logging is not enabled, eliminating all overhead in the common case.

Fixes #1096

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:57:38 +01:00

137 lines
3.8 KiB
Go

package update
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/ddns-updater/internal/update/mock_update"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_LogClient(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
requestMethod string
requetsHeaders http.Header
requestBodyNil bool
requestBodyString string
requestLineRegex string
responseStatusCode int
responseBodyNil bool
responseBodyString string
responseLineRegex string
}{
"PUT with headers and body": {
requestMethod: http.MethodPut,
requetsHeaders: http.Header{
"Key1": []string{"value 1", "value 2"},
"Key2": []string{"value 3"},
},
requestBodyString: "request body",
requestLineRegex: "PUT http://127.0.0.1:[0-9]{0,5} | " +
"headers: Key1: value1,value 2; Key2: value 3 | " +
"body: request body",
responseStatusCode: http.StatusAccepted,
responseBodyString: "response body",
responseLineRegex: "202 Accepted | " +
"headers: Content-Length: 9; Content-Type: text/plain; charset=utf-8; Date: .+ | " +
"body: response body",
},
"simple GET": {
requestMethod: http.MethodGet,
requestBodyNil: true,
requestLineRegex: "GET http://127.0.0.1:[0-9]{0,5}",
responseStatusCode: http.StatusOK,
responseBodyString: "response body",
responseLineRegex: "200 OK | " +
"headers: Date: .+; Content-Length: 13; Content-Type: text/plain; charset=utf-8 | " +
"body: response body",
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
handler := http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
// Check request matches the request we sent
assert.Equal(t, testCase.requestMethod, request.Method)
for key, expectedValues := range testCase.requetsHeaders {
values := request.Header[key]
assert.Equal(t, expectedValues, values)
}
if testCase.requestBodyNil {
require.Equal(t, http.NoBody, request.Body)
} else {
require.NotNil(t, request.Body)
b, err := io.ReadAll(request.Body)
require.NoError(t, err)
assert.Equal(t, testCase.requestBodyString, string(b))
}
// Send the response
rw.WriteHeader(testCase.responseStatusCode)
if testCase.responseBodyNil {
return
}
_, err := rw.Write([]byte(testCase.responseBodyString))
require.NoError(t, err)
})
server := httptest.NewServer(handler)
client := server.Client()
logger := mock_update.NewMockDebugLogger(ctrl)
logger.EXPECT().Debug(gomock.AssignableToTypeOf("")).
DoAndReturn(func(s string) {
assert.Regexp(t, testCase.requestLineRegex, s)
})
logger.EXPECT().Debug(gomock.AssignableToTypeOf("")).
DoAndReturn(func(s string) {
assert.Regexp(t, testCase.responseLineRegex, s)
})
logClient := makeLogClient(client, logger, true)
assert.Same(t, logClient, client)
ctx := context.Background()
var requestBody io.Reader
if !testCase.requestBodyNil {
requestBody = bytes.NewBufferString(testCase.requestBodyString)
}
request, err := http.NewRequestWithContext(ctx,
testCase.requestMethod, server.URL, requestBody)
require.NoError(t, err)
request.Header = testCase.requetsHeaders
response, err := logClient.Do(request)
require.NoError(t, err)
defer require.NoError(t, response.Body.Close())
// Ensure response received is as expected
assert.Equal(t, testCase.responseStatusCode, response.StatusCode)
if testCase.responseBodyNil {
assert.Nil(t, response.Body)
return
}
b, err := io.ReadAll(response.Body)
require.NoError(t, err)
assert.Equal(t, testCase.responseBodyString, string(b))
})
}
}