Browse Source

Added more test utilities for client_test.go, added NAMES and MODE handling for channels

master
Gisle Aune 7 years ago
parent
commit
6256f6a6fe
  1. 53
      channel.go
  2. 40
      client.go
  3. 38
      client_test.go
  4. 32
      internal/irctest/assert.go
  5. 90
      internal/irctest/interaction.go
  6. 24
      isupport/isupport.go

53
channel.go

@ -1,11 +1,15 @@
package irc 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 // A Channel is a target that manages the userlist
type Channel struct { type Channel struct {
name string name string
userlist list.List
userlist *list.List
} }
// Kind returns "channel" // 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}) 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
}
}
}
} }
} }

40
client.go

@ -18,6 +18,7 @@ import (
"git.aiterp.net/gisle/irc/ircutil" "git.aiterp.net/gisle/irc/ircutil"
"git.aiterp.net/gisle/irc/isupport" "git.aiterp.net/gisle/irc/isupport"
"git.aiterp.net/gisle/irc/list"
) )
var supportedCaps = []string{ var supportedCaps = []string{
@ -789,13 +790,13 @@ func (client *Client) handleEvent(event *Event) {
client.handleInTargets(event.Nick, event) client.handleInTargets(event.Nick, event)
} }
// Join/part handling
// Channel join/leave/mode handling
case "packet.join": case "packet.join":
{ {
var channel *Channel var channel *Channel
if event.Nick == client.nick { 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) client.AddTarget(channel)
} else { } else {
channel = client.Channel(event.Arg(0)) channel = client.Channel(event.Arg(0))
@ -829,12 +830,44 @@ func (client *Client) handleEvent(event *Event) {
client.handleInTargets(event.Nick, 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 // Account handling
case "packet.account": case "packet.account":
{ {
client.handleInTargets(event.Nick, event) 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) { 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 { if target.user.Nick == nick {
target.Handle(event, client) target.Handle(event, client)
event.targets = append(event.targets, target)
} }
} }
case *Status: case *Status:
{ {
if client.nick == event.Nick { if client.nick == event.Nick {
target.Handle(event, client) target.Handle(event, client)
event.targets = append(event.targets, target)
} }
} }
} }
} }
client.mutex.RUnlock() client.mutex.RUnlock()
} }

38
client_test.go

@ -2,6 +2,7 @@ package irc_test
import ( import (
"context" "context"
"errors"
"testing" "testing"
"git.aiterp.net/gisle/irc" "git.aiterp.net/gisle/irc"
@ -13,7 +14,7 @@ func TestClient(t *testing.T) {
Nick: "Test", Nick: "Test",
User: "Tester", User: "Tester",
RealName: "...", RealName: "...",
Alternatives: []string{"Test2", "Test3", "Test4"},
Alternatives: []string{"Test2", "Test3", "Test4", "Test768"},
}) })
t.Logf("Client.ID = %#+v", client.ID()) 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: 'S', Data: ":testserver.example.com 443 * Test3 :Nick is not available"},
{Kind: 'C', Data: "NICK Test4"}, {Kind: 'C', Data: "NICK Test4"},
{Kind: 'S', Data: ":testserver.example.com 443 * Test4 :Nick is not available"}, {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: 'S', Data: ":testserver.example.com 001 Test768 :Welcome to the TestServer Internet Relay Chat Network test"},
{Kind: 'C', Data: "WHO Test768*"}, {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 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 :- - #Test :: Test Channel"},
{Kind: 'S', Data: ":testserver.example.com 372 Test768 :- - #Test2 :: Other 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: ":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: '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 { if fail != nil {
t.Error("Index:", fail.Index) t.Error("Index:", fail.Index)
t.Error("NetErr:", fail.NetErr) t.Error("NetErr:", fail.NetErr)
t.Error("CBErr:", fail.CBErr)
t.Error("Result:", fail.Result) t.Error("Result:", fail.Result)
if fail.Index >= 0 { if fail.Index >= 0 {
t.Error("Line.Kind:", interaction.Lines[fail.Index].Kind) t.Error("Line.Kind:", interaction.Lines[fail.Index].Kind)

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

90
internal/irctest/interaction.go

@ -49,54 +49,66 @@ func (interaction *Interaction) Listen() (addr string, err error) {
for i := 0; i < len(lines); i++ { for i := 0; i < len(lines); i++ {
line := 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 Index int
Result string Result string
NetErr error NetErr error
CBErr error
} }
// InteractionLine is part of an interaction, whether it is a line // InteractionLine is part of an interaction, whether it is a line
// that is sent to a client or a line expected from a client. // that is sent to a client or a line expected from a client.
type InteractionLine struct { type InteractionLine struct {
Kind byte
Data string
Kind byte
Data string
Callback func() error
} }

24
isupport/isupport.go

@ -218,6 +218,30 @@ func (isupport *ISupport) IsPermissionMode(flag rune) bool {
return strings.ContainsRune(isupport.modeOrder, flag) 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 // 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 // in the CHANMODES variable it fits into. If it's not found at all, it will
// return -1 // return -1

Loading…
Cancel
Save