Browse Source

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

master
Gisle Aune 6 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
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
}
}
}
}
}

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

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

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++ {
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
}

24
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

Loading…
Cancel
Save