package cluster import ( "bufio" "net" "strconv" "strings" "testing" "time" ) // A node whose whole greeting is a bare "login:" — no newline, no banner — and // which then demands a password must still log in. // // Reported on f5mzn.org:9000: telnet by hand worked, OpsLog sat at "connecting" // for ever. Two faults met there. The prompts carry no newline, and the read // loop only ever looked at complete LINES, so neither prompt was seen at all; // and the blind 1.5 s login never recorded that it had sent the callsign, which // left the password branch — gated on that flag — permanently switched off. // // The test speaks the node's side literally: "login:" with no newline, then // "password:" with no newline, then a spot. It asserts both answers arrive and // that the session reaches Connected without any welcome banner to lean on. func TestBareLoginAndPasswordPromptsWithoutNewlines(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer ln.Close() host, portStr, _ := net.SplitHostPort(ln.Addr().String()) port, err := strconv.Atoi(portStr) if err != nil { t.Fatal(err) } type answers struct{ login, pwd string } got := make(chan answers, 1) go func() { c, err := ln.Accept() if err != nil { return } defer c.Close() rd := bufio.NewReader(c) // No newline, exactly as the node sends it. if _, err := c.Write([]byte("login: ")); err != nil { return } call, err := rd.ReadString('\n') if err != nil { return } if _, err := c.Write([]byte("password: ")); err != nil { return } pwd, err := rd.ReadString('\n') if err != nil { return } got <- answers{strings.TrimSpace(call), strings.TrimSpace(pwd)} // Something to prove the link is live and parsing again afterwards. _, _ = c.Write([]byte("DX de F4BPO: 14074.0 OY1CT FT8 1234Z\r\n")) time.Sleep(time.Second) }() spots := make(chan Spot, 4) s := &session{ cfg: ServerConfig{Name: "pwd node", Host: host, Port: port, Password: "s3cret"}, login: "F4BPO", onSpot: func(sp Spot) { spots <- sp }, onLine: func(Line) {}, onStatus: func() {}, stopCh: make(chan struct{}), } done := make(chan error, 1) go func() { _, err := s.runOnce(); done <- err }() defer func() { close(s.stopCh); <-done }() select { case a := <-got: if a.login != "F4BPO" { t.Errorf("callsign sent = %q, want F4BPO", a.login) } if a.pwd != "s3cret" { t.Errorf("password sent = %q, want s3cret — the prompt carried no newline", a.pwd) } case <-time.After(10 * time.Second): t.Fatal("the node's newline-less prompts were never answered") } select { case sp := <-spots: if sp.DXCall != "OY1CT" { t.Errorf("spot from the wrong station: %+v", sp) } case <-time.After(5 * time.Second): t.Fatal("no spot after the login — the stream is not being parsed") } // Connected without a welcome banner: the handshake alone must be enough. if st := s.snapshot().State; st != StateConnected { t.Errorf("state = %q, want %q — the server would still show as connecting", st, StateConnected) } }