Files
ciimsproxy/cmd/main/main_test.go
T

513 lines
18 KiB
Go
Raw Normal View History

2026-07-08 15:17:07 +08:00
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
2026-07-08 15:17:07 +08:00
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gzzn.com/mini/ciimsproxy/internal"
)
const (
testErrMsg = `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><soap:Fault><detail><BHIAFault><errorMessage xmlns="http://msg.ciims.bhia.itdcl.com">Can not find the event [FLOP-ESTT-ATC-ALL1]</errorMessage></BHIAFault></detail></soap:Fault></soap:Body></soap:Envelope>`
testSendOK = `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns1:sendResponse xmlns:ns1="http://ciims.bhia.itdcl.com/ExchangeService" /></soap:Body></soap:Envelope>`
testReceiveResp = `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns1:receiveResponse xmlns:ns1="http://ciims.bhia.itdcl.com/ExchangeService"><ns1:out><ns1:string>&lt;?xml version="1.0" encoding="UTF-8"?&gt;&lt;MSG&gt;&lt;A&gt;1&lt;/A&gt;&lt;/MSG&gt;</ns1:string></ns1:out></ns1:receiveResponse></soap:Body></soap:Envelope>`
)
func testRouter(t *testing.T, handler http.HandlerFunc) *gin.Engine {
t.Helper()
gin.SetMode(gin.TestMode)
ciims := httptest.NewServer(handler)
t.Cleanup(ciims.Close)
client, err := internal.NewClient(10)
require.NoError(t, err)
return newRouter(Config{ServerURL: ciims.URL, Listen: ":0", Timeout: 10, Client: client})
}
func performJSON(r http.Handler, method, path, body string) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
// =============================================================================
// Phase 2: Pure Function Tests — normalizeBaseURL
// =============================================================================
func TestNormalizeBaseURL_ValidHTTP(t *testing.T) {
result, err := normalizeBaseURL("http://example.com/path/")
require.NoError(t, err)
assert.Equal(t, "http://example.com/path", result)
}
func TestNormalizeBaseURL_ValidHTTPS(t *testing.T) {
result, err := normalizeBaseURL("https://EXAMPLE.COM:8443")
require.NoError(t, err)
assert.Equal(t, "https://example.com:8443", result)
}
func TestNormalizeBaseURL_Empty(t *testing.T) {
_, err := normalizeBaseURL("")
require.Error(t, err)
assert.Contains(t, err.Error(), "must not be empty")
}
func TestNormalizeBaseURL_WhitespaceOnly(t *testing.T) {
_, err := normalizeBaseURL(" ")
require.Error(t, err)
}
func TestNormalizeBaseURL_InvalidScheme(t *testing.T) {
_, err := normalizeBaseURL("ftp://example.com")
require.Error(t, err)
assert.Contains(t, err.Error(), "scheme must be http or https")
}
func TestNormalizeBaseURL_MissingHost(t *testing.T) {
_, err := normalizeBaseURL("http:///path")
require.Error(t, err)
assert.Contains(t, err.Error(), "host is required")
}
func TestNormalizeBaseURL_QueryNotAllowed(t *testing.T) {
_, err := normalizeBaseURL("http://example.com?a=1")
require.Error(t, err)
assert.Contains(t, err.Error(), "query and fragment")
}
func TestNormalizeBaseURL_FragmentNotAllowed(t *testing.T) {
_, err := normalizeBaseURL("http://example.com#section")
require.Error(t, err)
assert.Contains(t, err.Error(), "query and fragment")
}
func TestNormalizeBaseURL_InvalidURL(t *testing.T) {
_, err := normalizeBaseURL("://bad")
require.Error(t, err)
}
// =============================================================================
// Phase 2: Pure Function Tests — parseAllowedServers
// =============================================================================
func TestParseAllowedServers_Empty(t *testing.T) {
result, err := parseAllowedServers("")
require.NoError(t, err)
assert.Nil(t, result)
}
func TestParseAllowedServers_WhitespaceOnly(t *testing.T) {
_, err := parseAllowedServers(" , ")
require.Error(t, err)
assert.Contains(t, err.Error(), "CIIMS_ALLOWED_SERVERS")
}
func TestParseAllowedServers_ValidSingle(t *testing.T) {
result, err := parseAllowedServers("http://other.example.com")
require.NoError(t, err)
assert.Equal(t, []string{"http://other.example.com"}, result)
}
func TestParseAllowedServers_ValidMultiple(t *testing.T) {
result, err := parseAllowedServers("http://a.com,https://b.com:8443")
require.NoError(t, err)
assert.ElementsMatch(t, []string{"http://a.com", "https://b.com:8443"}, result)
}
func TestParseAllowedServers_InvalidEntry(t *testing.T) {
_, err := parseAllowedServers("http://good.com,ftp://bad.com")
require.Error(t, err)
assert.Contains(t, err.Error(), "CIIMS_ALLOWED_SERVERS")
assert.Contains(t, err.Error(), "ftp://bad.com")
}
// =============================================================================
// Phase 2: Pure Function Tests — getIntEnv
// =============================================================================
func TestGetIntEnv_Unset(t *testing.T) {
t.Setenv("CIIMS_TEST_UNSET_KEY", "")
result, err := getIntEnv("CIIMS_TEST_UNSET_KEY")
require.NoError(t, err)
assert.Equal(t, 0, result)
}
func TestGetIntEnv_Valid(t *testing.T) {
t.Setenv("CIIMS_TEST_VALID_KEY", "30")
result, err := getIntEnv("CIIMS_TEST_VALID_KEY")
require.NoError(t, err)
assert.Equal(t, 30, result)
}
func TestGetIntEnv_NotAnInteger(t *testing.T) {
t.Setenv("CIIMS_TEST_BAD_KEY", "abc")
_, err := getIntEnv("CIIMS_TEST_BAD_KEY")
require.Error(t, err)
assert.Contains(t, err.Error(), "must be an integer")
}
func TestGetIntEnv_Negative(t *testing.T) {
t.Setenv("CIIMS_TEST_NEG_KEY", "-5")
_, err := getIntEnv("CIIMS_TEST_NEG_KEY")
require.Error(t, err)
assert.Contains(t, err.Error(), "must be positive")
}
func TestGetIntEnv_Zero(t *testing.T) {
t.Setenv("CIIMS_TEST_ZERO_KEY", "0")
_, err := getIntEnv("CIIMS_TEST_ZERO_KEY")
require.Error(t, err)
assert.Contains(t, err.Error(), "must be positive")
}
// =============================================================================
// Phase 3: Handler-Level Tests (remaining gaps)
// =============================================================================
func TestSendNetworkError(t *testing.T) {
gin.SetMode(gin.TestMode)
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testSendOK)
}))
ciims.Close() // close before creating router so the URL is dead
client, err := internal.NewClient(1)
require.NoError(t, err)
r := newRouter(Config{
ServerURL: ciims.URL,
Listen: ":0",
Timeout: 1,
Client: client,
AllowedServers: map[string]string{ciims.URL: ciims.URL},
})
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "connection refused")
}
func TestSendInvalidJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
client, err := internal.NewClient(10)
require.NoError(t, err)
r := newRouter(Config{
ServerURL: "http://127.0.0.1:1",
Listen: ":0",
Timeout: 10,
Client: client,
AllowedServers: map[string]string{},
})
w := performJSON(r, http.MethodPost, "/send", `not json`)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestReceiveCountValidBoundary(t *testing.T) {
gin.SetMode(gin.TestMode)
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testReceiveResp)
}))
defer ciims.Close()
client, err := internal.NewClient(10)
require.NoError(t, err)
r := newRouter(Config{
ServerURL: ciims.URL,
Listen: ":0",
Timeout: 10,
Client: client,
AllowedServers: map[string]string{ciims.URL: ciims.URL},
})
w := performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":1000}`)
assert.Equal(t, http.StatusOK, w.Code)
}
// =============================================================================
// Phase 4: Config & Router Tests (remaining gaps)
// =============================================================================
func TestLoadConfig_Minimal(t *testing.T) {
t.Setenv("CIIMS_SERVER", "http://example.com")
t.Setenv("PROXY_LISTEN", "")
t.Setenv("CIIMS_TIMEOUT", "")
t.Setenv("CIIMS_ALLOWED_SERVERS", "")
config, err := loadConfig()
require.NoError(t, err)
assert.Equal(t, ":9090", config.Listen)
assert.Equal(t, 240, config.Timeout)
assert.Equal(t, "http://example.com", config.ServerURL)
assert.Contains(t, config.AllowedServers, "http://example.com")
require.NotNil(t, config.Client)
}
func TestLoadConfig_Full(t *testing.T) {
t.Setenv("CIIMS_SERVER", "http://ciims.example.com")
t.Setenv("PROXY_LISTEN", ":8080")
t.Setenv("CIIMS_TIMEOUT", "120")
t.Setenv("CIIMS_ALLOWED_SERVERS", "http://backup1.example.com,https://backup2.example.com:8443")
config, err := loadConfig()
require.NoError(t, err)
assert.Equal(t, ":8080", config.Listen)
assert.Equal(t, 120, config.Timeout)
assert.Equal(t, "http://ciims.example.com", config.ServerURL)
assert.Contains(t, config.AllowedServers, "http://ciims.example.com")
assert.Contains(t, config.AllowedServers, "http://backup1.example.com")
assert.Contains(t, config.AllowedServers, "https://backup2.example.com:8443")
require.NotNil(t, config.Client)
}
func TestLoadConfig_InvalidTimeout(t *testing.T) {
t.Setenv("CIIMS_SERVER", "http://example.com")
t.Setenv("CIIMS_TIMEOUT", "abc")
t.Setenv("CIIMS_ALLOWED_SERVERS", "")
_, err := loadConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "CIIMS_TIMEOUT")
}
func TestNewServerConfig(t *testing.T) {
client, err := internal.NewClient(30)
require.NoError(t, err)
config := Config{
ServerURL: "http://example.com",
AllowedServers: map[string]string{"http://example.com": "http://example.com"},
Listen: ":9999",
Timeout: 30,
Client: client,
}
srv := newServer(config)
assert.Equal(t, ":9999", srv.Addr)
assert.Equal(t, readHeaderTimeout, srv.ReadHeaderTimeout)
assert.Equal(t, readTimeout, srv.ReadTimeout)
assert.Equal(t, idleTimeout, srv.IdleTimeout)
assert.NotNil(t, srv.Handler)
}
func TestNewRouter_RoutesExist(t *testing.T) {
gin.SetMode(gin.TestMode)
client, err := internal.NewClient(10)
require.NoError(t, err)
config := Config{
ServerURL: "http://example.com",
AllowedServers: map[string]string{"http://example.com": "http://example.com"},
Listen: ":0",
Timeout: 10,
Client: client,
}
r := newRouter(config)
// Ping route
2026-07-08 15:17:07 +08:00
w := performJSON(r, http.MethodGet, "/ping", "")
assert.Equal(t, http.StatusOK, w.Code)
// Send route (will fail with 400 due to missing fields, but not 404)
w = performJSON(r, http.MethodPost, "/send", `{}`)
assert.NotEqual(t, http.StatusNotFound, w.Code)
// Receive route (will fail with 400 due to missing fields, but not 404)
w = performJSON(r, http.MethodPost, "/receive", `{}`)
assert.NotEqual(t, http.StatusNotFound, w.Code)
2026-07-08 15:17:07 +08:00
}
func TestSendSuccess(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testSendOK)
})
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusOK, w.Code)
assert.JSONEq(t, `{"error":""}`, w.Body.String())
}
func TestSendSOAPFault(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testErrMsg)
})
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Can not find the event")
}
func TestSendMissingRequiredField(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSendOversizedBody(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
body := `{"user":"FIMS","pass":"x","event":"E1","msg":"` + strings.Repeat("a", maxMsgLen) + `"}`
req := httptest.NewRequest(http.MethodPost, "/send", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusRequestEntityTooLarge, w.Code)
}
func TestReceiveSuccess(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testReceiveResp)
})
w := performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":2}`)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "MSG")
}
func TestReceiveInvalidCount(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
w := performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":0}`)
assert.Equal(t, http.StatusBadRequest, w.Code)
w = performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":1001}`)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestReceiveSOAPFault(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testErrMsg)
})
w := performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":2}`)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Can not find the event")
}
func TestHTTP500WithSOAPFaultReturnsFaultText(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, testErrMsg)
})
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Can not find the event")
assert.NotContains(t, w.Body.String(), "ciims returned 500")
}
func TestLoadConfigRequiresCIIMSServer(t *testing.T) {
t.Setenv("CIIMS_SERVER", "")
t.Setenv("CIIMS_TIMEOUT", "")
t.Setenv("CIIMS_ALLOWED_SERVERS", "")
_, err := loadConfig()
assert.Error(t, err)
assert.Contains(t, err.Error(), "CIIMS_SERVER")
}
func TestLoadConfigRejectsInvalidCIIMSServer(t *testing.T) {
t.Setenv("CIIMS_SERVER", "ftp://ciims.example.com")
t.Setenv("CIIMS_TIMEOUT", "")
t.Setenv("CIIMS_ALLOWED_SERVERS", "")
_, err := loadConfig()
assert.Error(t, err)
assert.Contains(t, err.Error(), "scheme")
}
func TestLoadConfigRejectsNonPositiveTimeout(t *testing.T) {
t.Setenv("CIIMS_SERVER", "http://ciims.example.com")
t.Setenv("CIIMS_TIMEOUT", "0")
t.Setenv("CIIMS_ALLOWED_SERVERS", "")
_, err := loadConfig()
assert.Error(t, err)
assert.Contains(t, err.Error(), "positive")
}
func TestLoadConfigAllowedServers(t *testing.T) {
t.Setenv("CIIMS_SERVER", "HTTP://ciims.example.com/base/")
t.Setenv("CIIMS_ALLOWED_SERVERS", "https://backup.example.com/ciims/")
t.Setenv("CIIMS_TIMEOUT", "3")
config, err := loadConfig()
require.NoError(t, err)
assert.Equal(t, "http://ciims.example.com/base", config.ServerURL)
assert.Contains(t, config.AllowedServers, "http://ciims.example.com/base")
assert.Contains(t, config.AllowedServers, "https://backup.example.com/ciims")
assert.Equal(t, 3, config.Timeout)
}
func TestLoadConfigRejectsInvalidAllowedServer(t *testing.T) {
t.Setenv("CIIMS_SERVER", "http://ciims.example.com")
t.Setenv("CIIMS_ALLOWED_SERVERS", "http://bad.example.com?x=1")
t.Setenv("CIIMS_TIMEOUT", "")
_, err := loadConfig()
assert.Error(t, err)
assert.Contains(t, err.Error(), "CIIMS_ALLOWED_SERVERS")
}
func TestNewServerTimeouts(t *testing.T) {
client, err := internal.NewClient(10)
require.NoError(t, err)
config := Config{ServerURL: "http://ciims.example.com", Listen: ":0", Timeout: 10, Client: client}
srv := newServer(config)
assert.Equal(t, readHeaderTimeout, srv.ReadHeaderTimeout)
assert.Equal(t, readTimeout, srv.ReadTimeout)
assert.Equal(t, 20*time.Second, srv.WriteTimeout)
assert.Equal(t, idleTimeout, srv.IdleTimeout)
}
func TestDefaultURLUsesCIIMSServer(t *testing.T) {
var gotPath string
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
fmt.Fprint(w, testSendOK)
})
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, servicePrefix, gotPath)
}
func TestAllowedRequestURLSucceeds(t *testing.T) {
gin.SetMode(gin.TestMode)
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testSendOK)
}))
defer ciims.Close()
client, err := internal.NewClient(10)
require.NoError(t, err)
r := newRouter(Config{
ServerURL: "http://default.example.com",
Listen: ":0",
Timeout: 10,
Client: client,
AllowedServers: map[string]string{ciims.URL: ciims.URL, "http://default.example.com": "http://default.example.com"},
})
body := fmt.Sprintf(`{"url":%q,"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`, ciims.URL+"/")
w := performJSON(r, http.MethodPost, "/send", body)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestDisallowedRequestURLReturns400AndDoesNotCallBackend(t *testing.T) {
calls := 0
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
calls++
fmt.Fprint(w, testSendOK)
})
w := performJSON(r, http.MethodPost, "/send", `{"url":"http://evil.example.com","user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, 0, calls)
}
func TestRequestURLWithQueryReturns400(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
w := performJSON(r, http.MethodPost, "/send", `{"url":"http://evil.example.com?x=1","user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestHTTP500WithoutSOAPFaultReturnsStatusError(t *testing.T) {
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "backend failed", http.StatusInternalServerError)
})
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "ciims returned 500")
}