diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..fdc2fc9 --- /dev/null +++ b/client_test.go @@ -0,0 +1,107 @@ +package irc_test + +import ( + "context" + "testing" + + "git.aiterp.net/gisle/irc" + "git.aiterp.net/gisle/irc/internal/irctest" +) + +func TestClientInteraction(t *testing.T) { + client := irc.New(context.Background(), irc.Config{ + Nick: "Test", + User: "Tester", + RealName: "...", + Alternatives: []string{"Test2", "Test3", "Test4"}, + }) + + interaction := irctest.Interaction{ + Strict: false, + Lines: []irctest.InteractionLine{ + {Kind: 'C', Data: "CAP LS 302"}, + {Kind: 'C', Data: "NICK Test"}, + {Kind: 'C', Data: "USER Tester 8 * :..."}, + {Kind: 'S', Data: ":testserver.example.com CAP * LS :multi-prefix userhost-in-names"}, + {Kind: 'C', Data: "CAP REQ :multi-prefix userhost-in-names"}, + {Kind: 'S', Data: ":testserver.example.com CAP * ACK :multi-prefix userhost-in-names"}, + {Kind: 'C', Data: "CAP END"}, + {Kind: 'S', Data: ":testserver.example.com 443 * Test :Nick is not available"}, + {Kind: 'C', Data: "NICK Test2"}, + {Kind: 'S', Data: ":testserver.example.com 443 * Test2 :Nick is not available"}, + {Kind: 'C', Data: "NICK Test3"}, + {Kind: 'S', Data: ":testserver.example.com 443 * Test3 :Nick is not available"}, + {Kind: 'C', Data: "NICK Test4"}, + {Kind: 'S', Data: ":testserver.example.com 443 * Test4 :Nick is not available"}, + {Kind: 'C', Data: "NICK Test*"}, + {Kind: 'S', Data: ":testserver.example.com 001 Test768 :Welcome to the TestServer Internet Relay Chat Network test"}, + {Kind: 'C', Data: "WHO Test768*"}, + {Kind: 'S', Data: ":testserver.example.com 002 Test768 :Your host is testserver.example.com[testserver.example.com/6667], running version charybdis-4-rc3"}, + {Kind: 'S', Data: ":testserver.example.com 003 Test768 :This server was created Fri Nov 25 2016 at 17:28:20 CET"}, + {Kind: 'S', Data: ":testserver.example.com 004 Test768 testserver.example.com charybdis-4-rc3 DQRSZagiloswxz CFILNPQbcefgijklmnopqrstvz bkloveqjfI"}, + {Kind: 'S', Data: ":testserver.example.com 005 Test768 FNC SAFELIST ELIST=CTU MONITOR=100 WHOX ETRACE KNOCK CHANTYPES=#& EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLNPQcgimnprstz CHANLIMIT=#&:15 :are supported by this server"}, + {Kind: 'S', Data: ":testserver.example.com 005 Test768 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=TestServer STATUSMSG=@+ CALLERID=g CASEMAPPING=rfc1459 NICKLEN=30 MAXNICKLEN=31 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server"}, + {Kind: 'S', Data: ":testserver.example.com 005 Test768 TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,&acjmorsuxz| CLIENTVER=3.0 :are supported by this server"}, + {Kind: 'S', Data: ":testserver.example.com 251 Test768 :There are 0 users and 2 invisible on 1 servers"}, + {Kind: 'S', Data: ":testserver.example.com 254 Test768 1 :channels formed"}, + {Kind: 'S', Data: ":testserver.example.com 255 Test768 :I have 2 clients and 0 servers"}, + {Kind: 'S', Data: ":testserver.example.com 265 Test768 2 2 :Current local users 2, max 2"}, + {Kind: 'S', Data: ":testserver.example.com 266 Test768 2 2 :Current global users 2, max 2"}, + {Kind: 'S', Data: ":testserver.example.com 250 Test768 :Highest connection count: 2 (2 clients) (8 connections received)"}, + {Kind: 'S', Data: ":testserver.example.com 352 Test768 * ~Tester testclient.example.com testserver.example.com Test768 H :0 ..."}, + {Kind: 'S', Data: ":testserver.example.com 375 Test768 :- testserver.example.com Message of the Day - "}, + {Kind: 'S', Data: ":testserver.example.com 372 Test768 :- This server is only for testing irce, not chatting. If you happen"}, + {Kind: 'S', Data: ":testserver.example.com 372 Test768 :- to connect to it by accident, please disconnect immediately."}, + {Kind: 'S', Data: ":testserver.example.com 372 Test768 :- "}, + {Kind: 'S', Data: ":testserver.example.com 372 Test768 :- - #Test :: Test Channel"}, + {Kind: 'S', Data: ":testserver.example.com 372 Test768 :- - #Test2 :: Other Test Channel"}, + {Kind: 'S', Data: ":testserver.example.com 376 Test768 :End of /MOTD command."}, + {Kind: 'S', Data: ":test MODE Test768 :+i"}, + {Kind: 'C', Data: "JOIN #Test"}, + }, + } + + addr, err := interaction.Listen() + if err != nil { + t.Fatal("Listen:", err) + } + + irc.Handle(func(event *irc.Event, client *irc.Client) { + if event.Name() == "packet.376" { + client.SendQueued("JOIN #Test") + } + }) + + err = client.Connect(addr, false) + if err != nil { + t.Fatal("Connect:", err) + return + } + + interaction.Wait() + + fail := interaction.Failure + if fail != nil { + t.Error("Index:", fail.Index) + t.Error("NetErr:", fail.NetErr) + t.Error("Result:", fail.Result) + if fail.Index >= 0 { + t.Error("Line.Kind:", interaction.Lines[fail.Index].Kind) + t.Error("Line.Data:", interaction.Lines[fail.Index].Data) + } + } + + if client.Nick() != "Test768" { + t.Errorf("Nick: %#+v != %#+v (Expectation)", client.Nick(), "Test768") + } + if client.User() != "~Tester" { + t.Errorf("User: %#+v != %#+v (Expectation)", client.User(), "~Tester") + } + if client.Host() != "testclient.example.com" { + t.Errorf("Host: %#+v != %#+v (Expectation)", client.Host(), "testclient.example.com") + } + + for i, logLine := range interaction.Log { + t.Logf("Log[%d] = %#+v", i, logLine) + } +} diff --git a/internal/irctest/interaction.go b/internal/irctest/interaction.go new file mode 100644 index 0000000..38cc7f9 --- /dev/null +++ b/internal/irctest/interaction.go @@ -0,0 +1,124 @@ +package irctest + +import ( + "bufio" + "net" + "strings" + "sync" + "time" +) + +// An Interaction is a "simulated" server that will trigger the +// client. +type Interaction struct { + wg sync.WaitGroup + + Strict bool + Lines []InteractionLine + Log []string + Failure *InteractionFailure +} + +// Listen listens for a client in a separate goroutine. +func (interaction *Interaction) Listen() (addr string, err error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return "", err + } + + lines := make([]InteractionLine, len(interaction.Lines)) + copy(lines, interaction.Lines) + + go func() { + interaction.wg.Add(1) + defer interaction.wg.Done() + + conn, err := listener.Accept() + if err != nil { + interaction.Failure = &InteractionFailure{ + Index: -1, NetErr: err, + } + + return + } + + defer conn.Close() + + reader := bufio.NewReader(conn) + + for i := 0; i < len(lines); i++ { + line := lines[i] + + switch line.Kind { + case 'S': + { + _, err := conn.Write(append([]byte(line.Data), '\r', '\n')) + if err != nil { + interaction.Failure = &InteractionFailure{ + Index: i, NetErr: err, + } + return + } + } + case 'C': + { + conn.SetReadDeadline(time.Now().Add(time.Second)) + input, err := reader.ReadString('\n') + if err != nil { + interaction.Failure = &InteractionFailure{ + Index: i, NetErr: err, + } + return + } + input = strings.Replace(input, "\r", "", -1) + input = strings.Replace(input, "\n", "", 1) + + match := line.Data + success := false + + if strings.HasSuffix(match, "*") { + success = strings.HasPrefix(input, match[:len(match)-1]) + } else { + success = match == input + } + + interaction.Log = append(interaction.Log, input) + + if !success { + if !interaction.Strict { + i-- + continue + } + + interaction.Failure = &InteractionFailure{ + Index: i, Result: input, + } + return + } + } + } + } + }() + + return listener.Addr().String(), nil +} + +// Wait waits for the setup to be done. It's safe to check +// Failure after that. +func (interaction *Interaction) Wait() { + interaction.wg.Wait() +} + +// InteractionFailure signifies a test failure. +type InteractionFailure struct { + Index int + Result string + NetErr error +} + +// InteractionLine is part of an interaction, whether it is a line +// that is sent to a client or a line expected from a client. +type InteractionLine struct { + Kind byte + Data string +} diff --git a/internal/irctest/interaction_test.go b/internal/irctest/interaction_test.go new file mode 100644 index 0000000..05b52c4 --- /dev/null +++ b/internal/irctest/interaction_test.go @@ -0,0 +1,56 @@ +package irctest_test + +import ( + "net" + "testing" + + "git.aiterp.net/gisle/irc/internal/irctest" +) + +func TestInteraction(t *testing.T) { + interaction := irctest.Interaction{ + Lines: []irctest.InteractionLine{ + {Kind: 'C', Data: "FIRST MESSAGE"}, + {Kind: 'S', Data: "SERVER MESSAGE"}, + {Kind: 'C', Data: "SECOND MESSAGE"}, + }, + } + + addr, err := interaction.Listen() + if err != nil { + t.Fatal("Listen:", err) + } + + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatal("Dial:", err) + } + + _, err = conn.Write([]byte("FIRST MESSAGE\r\n")) + if err != nil { + t.Fatal("Write:", err) + } + + buffer := make([]byte, 64) + n, err := conn.Read(buffer) + if err != nil { + t.Fatal("Read:", err) + } + if string(buffer[:n]) != "SERVER MESSAGE\r\n" { + t.Fatal("Read not correct:", string(buffer[:n])) + } + + _, err = conn.Write([]byte("SECOND MESSAGE\r\n")) + if err != nil { + t.Fatal("Write 2:", err) + } + + interaction.Wait() + + if interaction.Failure != nil { + t.Error("Index:", interaction.Failure.Index) + t.Error("Result:", interaction.Failure.Result) + t.Error("NetErr:", interaction.Failure.NetErr) + t.FailNow() + } +}