2026-07-08 15:17:07 +08:00
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
2026-07-08 16:23:00 +08:00
"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><?xml version="1.0" encoding="UTF-8"?><MSG><A>1</A></MSG></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
}
2026-07-08 16:23:00 +08:00
// =============================================================================
// 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 )
2026-07-08 16:23:00 +08:00
// 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" )
}
2026-07-08 16:23:00 +08:00
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" )
}