From 6256f6a6fe5b3c7ab2d5fe49125f08230015f81d Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Mon, 25 Jun 2018 10:58:14 +0200 Subject: [PATCH] Added more test utilities for client_test.go, added NAMES and MODE handling for channels --- channel.go | 53 ++++++++++++++++++- client.go | 40 ++++++++++++++- client_test.go | 38 ++++++++++++-- internal/irctest/assert.go | 32 ++++++++++++ internal/irctest/interaction.go | 90 +++++++++++++++++++-------------- isupport/isupport.go | 24 +++++++++ 6 files changed, 232 insertions(+), 45 deletions(-) create mode 100644 internal/irctest/assert.go diff --git a/channel.go b/channel.go index 85e6594..cc889ba 100644 --- a/channel.go +++ b/channel.go @@ -1,11 +1,15 @@ package irc -import "git.aiterp.net/gisle/irc/list" +import ( + "strings" + + "git.aiterp.net/gisle/irc/list" +) // A Channel is a target that manages the userlist type Channel struct { name string - userlist list.List + userlist *list.List } // Kind returns "channel" @@ -62,5 +66,50 @@ func (channel *Channel) Handle(event *Event, client *Client) { channel.userlist.Patch(event.Nick, list.UserPatch{User: newUser, Host: newHost}) } + case "packet.353": // NAMES + { + channel.userlist.SetAutoSort(false) + tokens := strings.Split(event.Text, " ") + for _, token := range tokens { + channel.userlist.InsertFromNamesToken(token) + } + } + case "packet.366": // End of NAMES + { + channel.userlist.SetAutoSort(true) + } + case "packet.mode": + { + isupport := client.ISupport() + plus := false + argIndex := 2 + + for _, ch := range event.Arg(1) { + if ch == '+' { + plus = true + continue + } + if ch == '-' { + plus = false + continue + } + + arg := "" + if isupport.ModeTakesArgument(ch, plus) { + arg = event.Arg(argIndex) + argIndex++ + } + + if isupport.IsPermissionMode(ch) { + if plus { + channel.userlist.AddMode(arg, ch) + } else { + channel.userlist.RemoveMode(arg, ch) + } + } else { + // TODO: track non-permission modes + } + } + } } } diff --git a/client.go b/client.go index b50c8d4..cb88ada 100644 --- a/client.go +++ b/client.go @@ -18,6 +18,7 @@ import ( "git.aiterp.net/gisle/irc/ircutil" "git.aiterp.net/gisle/irc/isupport" + "git.aiterp.net/gisle/irc/list" ) var supportedCaps = []string{ @@ -789,13 +790,13 @@ func (client *Client) handleEvent(event *Event) { client.handleInTargets(event.Nick, event) } - // Join/part handling + // Channel join/leave/mode handling case "packet.join": { var channel *Channel if event.Nick == client.nick { - channel = &Channel{name: event.Arg(0)} + channel = &Channel{name: event.Arg(0), userlist: list.New(&client.isupport)} client.AddTarget(channel) } else { channel = client.Channel(event.Arg(0)) @@ -829,12 +830,44 @@ func (client *Client) handleEvent(event *Event) { client.handleInTargets(event.Nick, event) } + case "packet.353": // NAMES + { + channel := client.Channel(event.Arg(2)) + if channel != nil { + channel.Handle(event, client) + } + } + + case "packet.366": // End of NAMES + { + channel := client.Channel(event.Arg(1)) + if channel != nil { + channel.Handle(event, client) + } + } + + case "packet.mode": + { + targetName := event.Arg(0) + + if client.isupport.IsChannel(targetName) { + channel := client.Channel(targetName) + if channel != nil { + channel.Handle(event, client) + } + } + } + // Account handling case "packet.account": { client.handleInTargets(event.Nick, event) } } + + if len(event.targets) == 0 { + event.targets = append(event.targets, client.status) + } } func (client *Client) handleInTargets(nick string, event *Event) { @@ -857,16 +890,19 @@ func (client *Client) handleInTargets(nick string, event *Event) { { if target.user.Nick == nick { target.Handle(event, client) + event.targets = append(event.targets, target) } } case *Status: { if client.nick == event.Nick { target.Handle(event, client) + event.targets = append(event.targets, target) } } } } + client.mutex.RUnlock() } diff --git a/client_test.go b/client_test.go index 046e4d0..6b59b64 100644 --- a/client_test.go +++ b/client_test.go @@ -2,6 +2,7 @@ package irc_test import ( "context" + "errors" "testing" "git.aiterp.net/gisle/irc" @@ -13,7 +14,7 @@ func TestClient(t *testing.T) { Nick: "Test", User: "Tester", RealName: "...", - Alternatives: []string{"Test2", "Test3", "Test4"}, + Alternatives: []string{"Test2", "Test3", "Test4", "Test768"}, }) t.Logf("Client.ID = %#+v", client.ID()) @@ -38,7 +39,7 @@ func TestClient(t *testing.T) { {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: 'C', Data: "NICK Test768"}, {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"}, @@ -61,8 +62,38 @@ func TestClient(t *testing.T) { {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: 'S', Data: ":Test768 MODE Test768 :+i"}, {Kind: 'C', Data: "JOIN #Test"}, + {Kind: 'S', Data: ":Test768!~test@127.0.0.1 JOIN #Test *"}, + {Kind: 'S', Data: ":testserver.example.com 353 Test768 = #Test :Test768!~test@127.0.0.1 @+Gisle!gisle@gisle.me"}, + {Kind: 'S', Data: ":testserver.example.com 366 Test768 #Test :End of /NAMES list."}, + {Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +osv Test768 Test768"}, + {Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +N-s "}, + {Kind: 'S', Data: ":Test1234!~test2@172.17.37.1 JOIN #Test Test1234"}, + {Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +v Test1234"}, + {Kind: 'S', Data: "PING :archgisle.lan"}, + {Kind: 'C', Data: "PONG :archgisle.lan"}, + {Callback: func() error { + channel := client.Channel("#Test") + if channel == nil { + return errors.New("Channel #Test not found") + } + + err := irctest.AssertUserlist(t, channel, "@Gisle", "@Test768", "+Test1234") + if err != nil { + return err + } + + userTest1234, ok := channel.UserList().User("Test1234") + if !ok { + return errors.New("Test1234 not found") + } + if userTest1234.Account != "Test1234" { + return errors.New("Test1234 did not get account from extended-join") + } + + return nil + }}, }, } @@ -89,6 +120,7 @@ func TestClient(t *testing.T) { if fail != nil { t.Error("Index:", fail.Index) t.Error("NetErr:", fail.NetErr) + t.Error("CBErr:", fail.CBErr) t.Error("Result:", fail.Result) if fail.Index >= 0 { t.Error("Line.Kind:", interaction.Lines[fail.Index].Kind) diff --git a/internal/irctest/assert.go b/internal/irctest/assert.go new file mode 100644 index 0000000..acbbd4c --- /dev/null +++ b/internal/irctest/assert.go @@ -0,0 +1,32 @@ +package irctest + +import ( + "errors" + "strings" + "testing" + + "git.aiterp.net/gisle/irc" +) + +// AssertUserlist compares the userlist to a list of prefixed nicks +func AssertUserlist(t *testing.T, channel *irc.Channel, assertedOrder ...string) error { + users := channel.UserList().Users() + order := make([]string, 0, len(users)) + for _, user := range users { + order = append(order, user.PrefixedNick) + } + + orderA := strings.Join(order, ", ") + orderB := strings.Join(assertedOrder, ", ") + + if orderA != orderB { + t.Logf("Userlist: %s", orderA) + t.Logf("Asserted: %s", orderB) + + t.Fail() + + return errors.New("Userlists does not match") + } + + return nil +} diff --git a/internal/irctest/interaction.go b/internal/irctest/interaction.go index 38cc7f9..75f1f3b 100644 --- a/internal/irctest/interaction.go +++ b/internal/irctest/interaction.go @@ -49,54 +49,66 @@ func (interaction *Interaction) Listen() (addr string, err error) { 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, + if line.Data != "" { + 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 } - 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, + 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 } - return - } - input = strings.Replace(input, "\r", "", -1) - input = strings.Replace(input, "\n", "", 1) + input = strings.Replace(input, "\r", "", -1) + input = strings.Replace(input, "\n", "", 1) - match := line.Data - success := false + match := line.Data + success := false - if strings.HasSuffix(match, "*") { - success = strings.HasPrefix(input, match[:len(match)-1]) - } else { - success = match == input - } + if strings.HasSuffix(match, "*") { + success = strings.HasPrefix(input, match[:len(match)-1]) + } else { + success = match == input + } - interaction.Log = append(interaction.Log, input) + interaction.Log = append(interaction.Log, input) - if !success { - if !interaction.Strict { - i-- - continue - } + if !success { + if !interaction.Strict { + i-- + continue + } - interaction.Failure = &InteractionFailure{ - Index: i, Result: input, + interaction.Failure = &InteractionFailure{ + Index: i, Result: input, + } + return } - return } } } + + if line.Callback != nil { + err := line.Callback() + if err != nil { + interaction.Failure = &InteractionFailure{ + Index: i, CBErr: err, + } + return + } + } } }() @@ -114,11 +126,13 @@ type InteractionFailure struct { Index int Result string NetErr error + CBErr 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 + Kind byte + Data string + Callback func() error } diff --git a/isupport/isupport.go b/isupport/isupport.go index 4efdc72..eb18634 100644 --- a/isupport/isupport.go +++ b/isupport/isupport.go @@ -218,6 +218,30 @@ func (isupport *ISupport) IsPermissionMode(flag rune) bool { return strings.ContainsRune(isupport.modeOrder, flag) } +// ModeTakesArgument returns true if the mode takes an argument +func (isupport *ISupport) ModeTakesArgument(flag rune, plus bool) bool { + isupport.lock.RLock() + defer isupport.lock.RUnlock() + + // Permission modes always take an argument. + if strings.ContainsRune(isupport.modeOrder, flag) { + return true + } + + // Modes in category A and B always takes an argument + if strings.ContainsRune(isupport.chanModes[0], flag) || strings.ContainsRune(isupport.chanModes[1], flag) { + return true + } + + // Modes in category C only takes one when added + if plus && strings.ContainsRune(isupport.chanModes[1], flag) { + return true + } + + // Modes in category D and outside never does + return false +} + // ChannelModeType returns a number from 0 to 3 based on what block of mode // in the CHANMODES variable it fits into. If it's not found at all, it will // return -1