mirror of
https://github.com/gissleh/irc.git
synced 2026-10-02 09:35:34 +00:00
Added more test utilities for client_test.go, added NAMES and MODE handling for channels
This commit is contained in:
+51
-2
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-3
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -49,52 +49,64 @@ 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 {
|
if line.Data != "" {
|
||||||
case 'S':
|
switch line.Kind {
|
||||||
{
|
case 'S':
|
||||||
_, err := conn.Write(append([]byte(line.Data), '\r', '\n'))
|
{
|
||||||
if err != nil {
|
_, err := conn.Write(append([]byte(line.Data), '\r', '\n'))
|
||||||
interaction.Failure = &InteractionFailure{
|
if err != nil {
|
||||||
Index: i, NetErr: err,
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
if line.Callback != nil {
|
||||||
success := false
|
err := line.Callback()
|
||||||
|
if err != nil {
|
||||||
if strings.HasSuffix(match, "*") {
|
interaction.Failure = &InteractionFailure{
|
||||||
success = strings.HasPrefix(input, match[:len(match)-1])
|
Index: i, CBErr: err,
|
||||||
} 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
Kind byte
|
||||||
Data string
|
Data string
|
||||||
|
Callback func() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user