package ctcp import ( "encoding/json" "net" "testing" "time" "gorm.io/datatypes" "rocketgit.ru/rsmon/worker/app/models" ) type settings struct { Host string `json:"host,omitempty"` Port string `json:"port,omitempty"` Timeout int `json:"timeout,omitempty"` Count int `json:"count,omitempty"` PacketSize int `json:"packet_size,omitempty"` } func newCheck(t *testing.T, s settings, host string) *models.Check { t.Helper() raw, err := json.Marshal(s) if err != nil { t.Fatalf("marshal: %v", err) } return &models.Check{ Kind: "tcp", Monitor: &models.Monitor{Host: host}, Settings: datatypes.JSON(raw), } } // startListener brings up a TCP listener on 127.0.0.1:0 so tests can // reach a real port. Returns the listener and the resolved address. func startListener(t *testing.T) (net.Listener, string) { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } return ln, ln.Addr().String() } func TestPerformOK(t *testing.T) { ln, addr := startListener(t) defer ln.Close() host, port, err := net.SplitHostPort(addr) if err != nil { t.Fatalf("split addr: %v", err) } c := newCheck(t, settings{Port: port, Timeout: 1}, host) r := Perform(c) if r.State != stateOK { t.Fatalf("expected OK, got %s (err=%v)", r.State, r.Error) } if r.RemoteAddr == "" { t.Fatalf("expected RemoteAddr to be populated, got %q", r.RemoteAddr) } if !contains(r.Infos[0], "tcp") { t.Fatalf("expected info line about tcp probe, got %v", r.Infos) } } func TestPerformRefused(t *testing.T) { // Bind to 127.0.0.1:0 to find a free port, close immediately so // the next dial gets RST/CONNREFUSED. ln, addr := startListener(t) host, port, _ := net.SplitHostPort(addr) _ = ln.Close() // On some runners the OS reassigns the just-closed port to a // listener before our dial. Retry up to a few times to stabilise. var final net.Listener for i := 0; i < 3; i++ { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { continue } addr2 := l.Addr().String() h, p, _ := net.SplitHostPort(addr2) _ = l.Close() // Replace what we are about to dial with one we just freed. addr = addr2 host = h port = p break } _ = final c := newCheck(t, settings{Port: port, Timeout: 1}, host) r := Perform(c) if r.State != stateERR { t.Fatalf("expected ERR, got %s (err=%v)", r.State, r.Error) } if r.Error == nil { t.Fatalf("expected error, got nil") } } func TestPerformUnreachable(t *testing.T) { // 127.0.0.0/8 — using 127.0.0.99 should resolve but be a hard // "no route" on most platforms. The check should fail fast. c := newCheck(t, settings{Port: "65530", Timeout: 1}, "127.0.0.99") // Use a short timeout so the test does not hang on slow CI. r := Perform(c) if r.State == stateOK { t.Fatalf("expected ERR/WARN, got OK (this loopback should not answer)") } } func TestPerformDefaultPort(t *testing.T) { ln, addr := startListener(t) defer ln.Close() host, _, _ := net.SplitHostPort(addr) c := newCheck(t, settings{}, host) // port empty -> defaults to 80 r := Perform(c) // 80 is unlikely to answer on the loopback; we only care that // the check did not panic and reported something on failure. if r.State == "" { t.Fatalf("expected non-empty state, got %q", r.State) } } func TestPerformEmptyHost(t *testing.T) { c := newCheck(t, settings{Port: "80", Timeout: 1}, "") r := Perform(c) if r.State != stateERR { t.Fatalf("expected ERR on empty host, got %s", r.State) } if r.Error == nil { t.Fatalf("expected error, got nil") } } func TestPerformClampsTimeout(t *testing.T) { // Already covered indirectly by TestPerformDefaultPort but // assert the dialer budget is at least 1s even with Timeout=0. ln, addr := startListener(t) defer ln.Close() host, port, _ := net.SplitHostPort(addr) start := time.Now() c := newCheck(t, settings{Port: port, Timeout: 0}, host) r := Perform(c) elapsed := time.Since(start) if elapsed > 2*time.Second { t.Fatalf("default timeout exceeded 2s, got %v", elapsed) } if r.State != stateOK { t.Fatalf("expected OK against local listener, got %s", r.State) } } func contains(s, sub string) bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false }