Files
OpsLog/internal/cluster/login_test.go
T
2026-08-17 13:18:32 +02:00

106 lines
3.1 KiB
Go

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)
}
}