fix: comprehensive bug fixes, hardening, and test coverage
Bug fixes: - Fix nil-pointer panic in sendMessage SOAP fault handler (used err.Error() on nil) - Fix missing return after SOAP fault response (caused fall-through to 200 OK) - Fix timeout=0 shadowing: removed package-level constant, use configured value - Fix receiveMessage not checking SOAP faults from CIIMS Codec hardening: - Replace fragile regex parsing with encoding/xml.Decoder for namespace-agnostic XML - XML-escape all user inputs (user, pass, event) — not just message body - Remove magic-number-based string slicing (split, GetErrMsg) Transport improvements: - Extract Client type with connection pooling (reuse http.Client across requests) - Add CIIMSResponse domain type with IsFault(), ErrorMessage(), Messages() - Add HTTPStatusError and ResponseTooLargeError typed errors - Replace deprecated ioutil.ReadAll with io.ReadAll - Add response body size limit (64 MiB) Security hardening: - Add MaxBytesReader (1MB request body limit) on both endpoints - Add URL allowlist via CIIMS_ALLOWED_SERVERS env var - Add normalizeBaseURL with scheme/host/query validation - Add count bounds validation (1-1000) on /receive - Add server-level timeouts (ReadHeaderTimeout, ReadTimeout, IdleTimeout) Configuration: - Introduce Config struct to replace package-level globals - Add loadConfig() with full validation and error propagation - Add getIntEnv() with positive-value enforcement Test coverage (75 tests, 35 new): - Phase 1: 8 internal tests (nil receiver, error types, SOAP+HTTP500, malformed XML) - Phase 2: 19 pure function tests (normalizeBaseURL, parseAllowedServers, getIntEnv) - Phase 3: 16 handler tests (send/receive success, SOAP fault, network error, HTTP 500, body too large, invalid JSON, missing fields, URL allowlist) - Phase 4: 8 config/router tests (loadConfig, newServer, newRouter) Toolchain: - Upgrade Go 1.13 → 1.22, gin 1.6.3 → 1.10.0, testify 1.5.1 → 1.10.0 Cleanup: - Remove dead code (unused post() function, commented-out defaults) - Replace println with structured logging - Add .gitignore - Rewrite README with API docs, env vars, security considerations
This commit is contained in:
+111
-6
@@ -1,12 +1,14 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -22,7 +24,7 @@ func TestClientSend_SOAPFault(t *testing.T) {
|
||||
|
||||
client, err := NewClient(10)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
resp, err := client.Send(context.Background(), ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
@@ -39,7 +41,7 @@ func TestClientSend_Success(t *testing.T) {
|
||||
|
||||
client, err := NewClient(10)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
resp, err := client.Send(context.Background(), ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
@@ -55,7 +57,7 @@ func TestClientSend_ReceiveMessages(t *testing.T) {
|
||||
|
||||
client, err := NewClient(10)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
resp, err := client.Send(context.Background(), ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
@@ -80,7 +82,7 @@ func TestClientSend_NetworkTimeout(t *testing.T) {
|
||||
|
||||
client, err := NewClient(1)
|
||||
require.NoError(t, err)
|
||||
_, err = client.Send("http://"+addr+"/services/ExchangeService", "<dummy>")
|
||||
_, err = client.Send(context.Background(), "http://"+addr+"/services/ExchangeService", "<dummy>")
|
||||
listener.Close()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -88,7 +90,7 @@ func TestClientSend_NetworkTimeout(t *testing.T) {
|
||||
func TestClientSend_ServerError(t *testing.T) {
|
||||
client, err := NewClient(1)
|
||||
require.NoError(t, err)
|
||||
_, err = client.Send("http://127.0.0.1:1/nonexistent", "<dummy>")
|
||||
_, err = client.Send(context.Background(), "http://127.0.0.1:1/nonexistent", "<dummy>")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -100,7 +102,7 @@ func TestClientSend_HTTPStatusError(t *testing.T) {
|
||||
|
||||
client, err := NewClient(10)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
resp, err := client.Send(context.Background(), ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
require.Error(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
@@ -172,3 +174,106 @@ func TestCreateReceive_XMLEscapes(t *testing.T) {
|
||||
assert.Contains(t, result, "user<>&"'")
|
||||
assert.Contains(t, result, "pass>")
|
||||
}
|
||||
|
||||
func TestClientSend_ContextCanceled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
client, err := NewClient(10)
|
||||
require.NoError(t, err)
|
||||
_, err = client.Send(ctx, "http://127.0.0.1:1/nonexistent", "<dummy>")
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
func TestClientSend_ResponseTooLarge(t *testing.T) {
|
||||
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/xml; charset=UTF-8")
|
||||
_, _ = io.Copy(w, io.LimitReader(zeroReader{}, maxResponseBytes+1))
|
||||
}))
|
||||
defer ciims.Close()
|
||||
|
||||
client, err := NewClient(10)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Send(context.Background(), ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
|
||||
var tooLarge *ResponseTooLargeError
|
||||
assert.True(t, errors.As(err, &tooLarge))
|
||||
}
|
||||
|
||||
func TestNewClient_ZeroOrNegativeTimeout(t *testing.T) {
|
||||
_, err := NewClient(0)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "positive")
|
||||
|
||||
_, err = NewClient(-1)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "positive")
|
||||
}
|
||||
|
||||
func TestNewClient_ValidTimeout(t *testing.T) {
|
||||
client, err := NewClient(30)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
}
|
||||
|
||||
func TestHTTPStatusError_Truncation(t *testing.T) {
|
||||
body := strings.Repeat("x", 600)
|
||||
err := &HTTPStatusError{StatusCode: 500, Status: "500 Internal Server Error", Body: body}
|
||||
errStr := err.Error()
|
||||
assert.Contains(t, errStr, "...")
|
||||
assert.LessOrEqual(t, len(errStr), 570) // 46 prefix + 512 body + 3 ellipsis = 561
|
||||
}
|
||||
|
||||
func TestHTTPStatusError_EmptyBody(t *testing.T) {
|
||||
err := &HTTPStatusError{StatusCode: 502, Status: "502 Bad Gateway", Body: ""}
|
||||
assert.Equal(t, "ciims returned 502 Bad Gateway", err.Error())
|
||||
}
|
||||
|
||||
func TestResponseTooLargeError_Error(t *testing.T) {
|
||||
err := &ResponseTooLargeError{Limit: 100}
|
||||
assert.Equal(t, "ciims response exceeds 100 bytes", err.Error())
|
||||
}
|
||||
|
||||
func TestCIIMSResponse_NilReceiver(t *testing.T) {
|
||||
var nilResp *CIIMSResponse
|
||||
assert.False(t, nilResp.IsFault())
|
||||
assert.Equal(t, "", nilResp.ErrorMessage())
|
||||
assert.Nil(t, nilResp.Messages())
|
||||
}
|
||||
|
||||
func TestClientSend_SOAPFaultWithHTTP500(t *testing.T) {
|
||||
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/xml; charset=UTF-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, ErrMsg)
|
||||
}))
|
||||
defer ciims.Close()
|
||||
|
||||
client, err := NewClient(10)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Send(context.Background(), ciims.URL+"/services/ExchangeService", "<dummy>")
|
||||
require.Error(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
var statusErr *HTTPStatusError
|
||||
require.True(t, errors.As(err, &statusErr))
|
||||
assert.Equal(t, http.StatusInternalServerError, statusErr.StatusCode)
|
||||
assert.True(t, resp.IsFault())
|
||||
assert.Equal(t, "Can not find the event [FLOP-ESTT-ATC-ALL1]", resp.ErrorMessage())
|
||||
}
|
||||
|
||||
func TestXmlElementTexts_MalformedXML(t *testing.T) {
|
||||
msgs := GetMsgs("<soap><Body><string>incomplete")
|
||||
assert.Equal(t, 0, len(msgs))
|
||||
}
|
||||
|
||||
type zeroReader struct{}
|
||||
|
||||
func (zeroReader) Read(p []byte) (int, error) {
|
||||
for i := range p {
|
||||
p[i] = 'a'
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user