package main import ( "bytes" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gzzn.com/mini/ciimsproxy/internal" ) const ( testErrMsg = `Can not find the event [FLOP-ESTT-ATC-ALL1]` testSendOK = `` testReceiveResp = `<?xml version="1.0" encoding="UTF-8"?><MSG><A>1</A></MSG>` ) 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":""}`) 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 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) } 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":""}`) 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":""}`) 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":""}`) 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":""}`) 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":""}`) 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":""}`, 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":""}`) 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":""}`) 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":""}`) assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, w.Body.String(), "ciims returned 500") }