check / check (push) Waiting to run
The relay dropped every PRIVMSG, NOTICE, JOIN, PART, NICK, QUIT, TOPIC, MODE and KICK sent under the connection's own nick, so an IRC listener connection never saw what its session did from another client. The relay now drops nothing by nick. Instead, the service methods that fan out a user's own command take the client whose queue to skip: the IRC listener passes its own client, which has already written the echo, and the HTTP API passes 0. Channel messages and PARTs now also go to the sender's own session, so the HTTP handler no longer queues the channel-message echo itself, SendChannelMessage no longer returns the message's row ID, and neoirc-cli ignores the PART event for its own part, which it has already shown. Model: opus-5-5
1785 lines
33 KiB
Go
1785 lines
33 KiB
Go
package db_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/neoirc/internal/db"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func setupTestDB(t *testing.T) *db.Database {
|
|
t.Helper()
|
|
|
|
database, err := db.NewTestDatabase()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
closeErr := database.Close()
|
|
if closeErr != nil {
|
|
t.Logf("close db: %v", closeErr)
|
|
}
|
|
})
|
|
|
|
return database
|
|
}
|
|
|
|
func TestCreateSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sessionID, _, token, err := database.CreateSession(
|
|
ctx, "alice", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if sessionID == 0 || token == "" {
|
|
t.Fatal("expected valid id and token")
|
|
}
|
|
|
|
_, _, dupToken, dupErr := database.CreateSession(
|
|
ctx, "alice", "", "", "",
|
|
)
|
|
if dupErr == nil {
|
|
t.Fatal("expected error for duplicate nick")
|
|
}
|
|
|
|
_ = dupToken
|
|
}
|
|
|
|
// assertSessionHostInfo creates a session and verifies
|
|
// the stored username and hostname match expectations.
|
|
func assertSessionHostInfo(
|
|
t *testing.T,
|
|
database *db.Database,
|
|
nick, inputUser, inputHost,
|
|
expectUser, expectHost string,
|
|
) {
|
|
t.Helper()
|
|
|
|
sessionID, _, _, err := database.CreateSession(
|
|
t.Context(), nick, inputUser, inputHost, "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
info, err := database.GetSessionHostInfo(
|
|
t.Context(), sessionID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if info.Username != expectUser {
|
|
t.Fatalf(
|
|
"expected username %s, got %s",
|
|
expectUser, info.Username,
|
|
)
|
|
}
|
|
|
|
if info.Hostname != expectHost {
|
|
t.Fatalf(
|
|
"expected hostname %s, got %s",
|
|
expectHost, info.Hostname,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestCreateSessionWithUserHost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
|
|
assertSessionHostInfo(
|
|
t, database,
|
|
"hostuser", "myident", "example.com",
|
|
"myident", "example.com",
|
|
)
|
|
}
|
|
|
|
func TestCreateSessionDefaultUsername(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
|
|
// Empty username defaults to nick.
|
|
assertSessionHostInfo(
|
|
t, database,
|
|
"defaultu", "", "host.local",
|
|
"defaultu", "host.local",
|
|
)
|
|
}
|
|
|
|
func TestCreateSessionStoresIP(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sessionID, clientID, _, err := database.CreateSession(
|
|
ctx, "ipuser", "ident", "host.example.com",
|
|
"192.168.1.42",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
info, err := database.GetSessionHostInfo(
|
|
ctx, sessionID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if info.IP != "192.168.1.42" {
|
|
t.Fatalf(
|
|
"expected session IP 192.168.1.42, got %s",
|
|
info.IP,
|
|
)
|
|
}
|
|
|
|
clientInfo, err := database.GetClientHostInfo(
|
|
ctx, clientID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if clientInfo.IP != "192.168.1.42" {
|
|
t.Fatalf(
|
|
"expected client IP 192.168.1.42, got %s",
|
|
clientInfo.IP,
|
|
)
|
|
}
|
|
|
|
if clientInfo.Hostname != "host.example.com" {
|
|
t.Fatalf(
|
|
"expected client hostname host.example.com, got %s",
|
|
clientInfo.Hostname,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGetClientHostInfoNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
|
|
_, err := database.GetClientHostInfo(
|
|
t.Context(), 99999,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error for nonexistent client")
|
|
}
|
|
}
|
|
|
|
func TestGetSessionHostInfoNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
|
|
_, err := database.GetSessionHostInfo(
|
|
t.Context(), 99999,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestFormatHostmask(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result := db.FormatHostmask(
|
|
"nick", "user", "host.com",
|
|
)
|
|
if result != "nick!user@host.com" {
|
|
t.Fatalf(
|
|
"expected nick!user@host.com, got %s",
|
|
result,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestFormatHostmaskDefaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result := db.FormatHostmask("nick", "", "")
|
|
if result != "nick!nick@*" {
|
|
t.Fatalf(
|
|
"expected nick!nick@*, got %s",
|
|
result,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestMemberInfoHostmask(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
member := &db.MemberInfo{ //nolint:exhaustruct // test only uses hostmask fields
|
|
Nick: "alice",
|
|
Username: "aliceident",
|
|
Hostname: "alice.example.com",
|
|
}
|
|
|
|
hostmask := member.Hostmask()
|
|
expected := "alice!aliceident@alice.example.com"
|
|
|
|
if hostmask != expected {
|
|
t.Fatalf(
|
|
"expected %s, got %s", expected, hostmask,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelMembersIncludeUserHost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, _, _, err := database.CreateSession(
|
|
ctx, "memuser", "myuser", "myhost.net", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chID, err := database.GetOrCreateChannel(
|
|
ctx, "#hostchan",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
members, err := database.ChannelMembers(ctx, chID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(members) != 1 {
|
|
t.Fatalf(
|
|
"expected 1 member, got %d", len(members),
|
|
)
|
|
}
|
|
|
|
if members[0].Username != "myuser" {
|
|
t.Fatalf(
|
|
"expected username myuser, got %s",
|
|
members[0].Username,
|
|
)
|
|
}
|
|
|
|
if members[0].Hostname != "myhost.net" {
|
|
t.Fatalf(
|
|
"expected hostname myhost.net, got %s",
|
|
members[0].Hostname,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGetSessionByToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
_, _, token, err := database.CreateSession(ctx, "bob", "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sessionID, clientID, nick, err :=
|
|
database.GetSessionByToken(ctx, token)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if nick != "bob" || sessionID == 0 || clientID == 0 {
|
|
t.Fatalf("expected bob, got %s", nick)
|
|
}
|
|
|
|
badSID, badCID, badNick, badErr :=
|
|
database.GetSessionByToken(ctx, "badtoken")
|
|
if badErr == nil {
|
|
t.Fatal("expected error for bad token")
|
|
}
|
|
|
|
if badSID != 0 || badCID != 0 || badNick != "" {
|
|
t.Fatal("expected zero values on error")
|
|
}
|
|
}
|
|
|
|
func TestGetSessionByNick(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
charlieID, charlieClientID, charlieToken, err :=
|
|
database.CreateSession(ctx, "charlie", "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if charlieID == 0 || charlieClientID == 0 {
|
|
t.Fatal("expected valid session/client IDs")
|
|
}
|
|
|
|
if charlieToken == "" {
|
|
t.Fatal("expected non-empty token")
|
|
}
|
|
|
|
id, err := database.GetSessionByNick(ctx, "charlie")
|
|
if err != nil || id == 0 {
|
|
t.Fatal("expected to find charlie")
|
|
}
|
|
|
|
_, err = database.GetSessionByNick(ctx, "nobody")
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown nick")
|
|
}
|
|
}
|
|
|
|
func TestChannelOperations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#test")
|
|
if err != nil || chID == 0 {
|
|
t.Fatal("expected channel id")
|
|
}
|
|
|
|
chID2, err := database.GetOrCreateChannel(ctx, "#test")
|
|
if err != nil || chID2 != chID {
|
|
t.Fatal("expected same channel id")
|
|
}
|
|
|
|
chID3, err := database.GetChannelByName(ctx, "#test")
|
|
if err != nil || chID3 != chID {
|
|
t.Fatal("expected same channel id")
|
|
}
|
|
|
|
_, err = database.GetChannelByName(ctx, "#nope")
|
|
if err == nil {
|
|
t.Fatal("expected error for nonexistent channel")
|
|
}
|
|
}
|
|
|
|
func TestJoinAndPart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, _, _, err := database.CreateSession(ctx, "user1", "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#chan")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ids, err := database.GetChannelMemberIDs(ctx, chID)
|
|
if err != nil || len(ids) != 1 || ids[0] != sid {
|
|
t.Fatal("expected session in channel")
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.PartChannelAndDeleteIfEmpty(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ids, _ = database.GetChannelMemberIDs(ctx, chID)
|
|
if len(ids) != 0 {
|
|
t.Fatal("expected empty channel")
|
|
}
|
|
}
|
|
|
|
// TestPartChannelAndDeleteIfEmpty checks that a part keeps
|
|
// a channel another session is still on and deletes the
|
|
// channel when its last member parts.
|
|
func TestPartChannelAndDeleteIfEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(
|
|
ctx, "#empty",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
nicks := []string{"temp1", "temp2"}
|
|
sids := make([]int64, 0, len(nicks))
|
|
|
|
for _, nick := range nicks {
|
|
sid, _, _, createErr := database.CreateSession(ctx, nick, "", "", "")
|
|
if createErr != nil {
|
|
t.Fatal(createErr)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sids = append(sids, sid)
|
|
}
|
|
|
|
err = database.PartChannelAndDeleteIfEmpty(ctx, chID, sids[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = database.GetChannelByName(ctx, "#empty")
|
|
if err != nil {
|
|
t.Fatalf("channel deleted while a member is on it: %v", err)
|
|
}
|
|
|
|
err = database.PartChannelAndDeleteIfEmpty(ctx, chID, sids[1])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = database.GetChannelByName(ctx, "#empty")
|
|
if err == nil {
|
|
t.Fatal("expected channel to be deleted")
|
|
}
|
|
}
|
|
|
|
func createSessionWithChannels(
|
|
t *testing.T,
|
|
database *db.Database,
|
|
nick, ch1Name, ch2Name string,
|
|
) (int64, int64, int64) {
|
|
t.Helper()
|
|
|
|
ctx := t.Context()
|
|
|
|
sid, _, _, err := database.CreateSession(ctx, nick, "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ch1, err := database.GetOrCreateChannel(
|
|
ctx, ch1Name,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ch2, err := database.GetOrCreateChannel(
|
|
ctx, ch2Name,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, ch1, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, ch2, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return sid, ch1, ch2
|
|
}
|
|
|
|
func TestListChannels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
sid, _, _ := createSessionWithChannels(
|
|
t, database, "lister", "#a", "#b",
|
|
)
|
|
|
|
channels, err := database.ListChannels(
|
|
t.Context(), sid,
|
|
)
|
|
if err != nil || len(channels) != 2 {
|
|
t.Fatalf(
|
|
"expected 2 channels, got %d",
|
|
len(channels),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestListAllChannels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
_, err := database.GetOrCreateChannel(ctx, "#x")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = database.GetOrCreateChannel(ctx, "#y")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
channels, err := database.ListAllChannels(ctx)
|
|
if err != nil || len(channels) < 2 {
|
|
t.Fatalf(
|
|
"expected >= 2 channels, got %d",
|
|
len(channels),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChangeNick(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, _, token, err := database.CreateSession(
|
|
ctx, "old", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.ChangeNick(ctx, sid, "new")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, _, nick, err := database.GetSessionByToken(
|
|
ctx, token,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if nick != "new" {
|
|
t.Fatalf("expected new, got %s", nick)
|
|
}
|
|
}
|
|
|
|
func TestSetTopic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
_, err := database.GetOrCreateChannel(
|
|
ctx, "#topictest",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.SetTopic(ctx, "#topictest", "Hello")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
channels, err := database.ListAllChannels(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, ch := range channels {
|
|
if ch.Name == "#topictest" &&
|
|
ch.Topic != "Hello" {
|
|
t.Fatalf(
|
|
"expected topic Hello, got %s",
|
|
ch.Topic,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInsertMessage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
body := json.RawMessage(`["hello"]`)
|
|
|
|
dbID, msgUUID, err := database.InsertMessage(
|
|
ctx, "PRIVMSG", "poller", "#test", 0, 0, nil, body, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if dbID == 0 || msgUUID == "" {
|
|
t.Fatal("expected valid id and uuid")
|
|
}
|
|
}
|
|
|
|
func TestPollMessages(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, _, token, err := database.CreateSession(
|
|
ctx, "poller", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, clientID, _, err := database.GetSessionByToken(
|
|
ctx, token,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body := json.RawMessage(`["hello"]`)
|
|
|
|
dbID, _, err := database.InsertMessage(
|
|
ctx, "PRIVMSG", "poller", "#test", 0, 0, nil, body, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.EnqueueToSession(ctx, sid, dbID, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
const batchSize = 10
|
|
|
|
msgs, lastQID, err := database.PollMessages(
|
|
ctx, clientID, 0, batchSize,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(msgs) != 1 {
|
|
t.Fatalf(
|
|
"expected 1 message, got %d", len(msgs),
|
|
)
|
|
}
|
|
|
|
if msgs[0].Command != "PRIVMSG" {
|
|
t.Fatalf(
|
|
"expected PRIVMSG, got %s", msgs[0].Command,
|
|
)
|
|
}
|
|
|
|
if lastQID == 0 {
|
|
t.Fatal("expected nonzero lastQID")
|
|
}
|
|
|
|
msgs, _, _ = database.PollMessages(
|
|
ctx, clientID, lastQID, batchSize,
|
|
)
|
|
|
|
if len(msgs) != 0 {
|
|
t.Fatalf(
|
|
"expected 0 messages, got %d", len(msgs),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGetHistory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#hist")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
const msgCount = 10
|
|
|
|
for range msgCount {
|
|
_, _, err = database.InsertMessage(
|
|
ctx, "PRIVMSG", "user", "#hist", 0, chID,
|
|
nil, json.RawMessage(`["msg"]`), nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
const histLimit = 5
|
|
|
|
msgs, err := database.GetHistory(
|
|
ctx, 0, chID, "", histLimit,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(msgs) != histLimit {
|
|
t.Fatalf("expected %d, got %d",
|
|
histLimit, len(msgs))
|
|
}
|
|
|
|
if msgs[0].DBID > msgs[histLimit-1].DBID {
|
|
t.Fatal("expected ascending order")
|
|
}
|
|
}
|
|
|
|
// TestGetHistoryReusedNames checks that history follows the
|
|
// session and the channel row, not the nick or the channel
|
|
// name: a session that takes a released nick, and a channel
|
|
// created again under a deleted one's name, start with none.
|
|
func TestGetHistoryReusedNames(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
historyLen := func(toSessionID, toChannelID int64) int {
|
|
t.Helper()
|
|
|
|
msgs, err := database.GetHistory(
|
|
ctx, toSessionID, toChannelID, "", 0,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return len(msgs)
|
|
}
|
|
|
|
oldSID, _, _, err := database.CreateSession(
|
|
ctx, "reused", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldChID, err := database.GetOrCreateChannel(ctx, "#reused")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, oldChID, oldSID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body := json.RawMessage(`["hi"]`)
|
|
|
|
_, _, err = database.InsertMessage(
|
|
ctx, "PRIVMSG", "sender", "reused", oldSID, 0,
|
|
nil, body, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, _, err = database.InsertMessage(
|
|
ctx, "PRIVMSG", "sender", "#reused", 0, oldChID,
|
|
nil, body, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if historyLen(oldSID, 0) != 1 || historyLen(0, oldChID) != 1 {
|
|
t.Fatal("expected one DM and one channel message")
|
|
}
|
|
|
|
// The session is the channel's only member, so quitting
|
|
// deletes the channel too.
|
|
_, err = database.QuitSession(ctx, oldSID, "reused", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newSID, _, _, err := database.CreateSession(
|
|
ctx, "reused", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newChID, err := database.GetOrCreateChannel(ctx, "#reused")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if historyLen(newSID, 0) != 0 || historyLen(0, newChID) != 0 {
|
|
t.Fatal("expected no history for the names' new holders")
|
|
}
|
|
}
|
|
|
|
// TestQuitSession checks that QuitSession deletes the
|
|
// session, keeps a channel another session is still on,
|
|
// deletes a channel it leaves with no members, and queues
|
|
// one QUIT to the other session; and that a second call for
|
|
// the same session changes nothing.
|
|
func TestQuitSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, sharedID, _ := createSessionWithChannels(
|
|
t, database, "deleteme", "#shared", "#alone",
|
|
)
|
|
|
|
peerSID, peerClientID, _, err := database.CreateSession(
|
|
ctx, "peer", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, sharedID, peerSID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body := json.RawMessage(`["bye"]`)
|
|
|
|
peerIDs, err := database.QuitSession(ctx, sid, "deleteme", body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !slices.Equal(peerIDs, []int64{peerSID}) {
|
|
t.Fatalf("got peers %v, want [%d]", peerIDs, peerSID)
|
|
}
|
|
|
|
_, err = database.GetSessionByNick(ctx, "deleteme")
|
|
if err == nil {
|
|
t.Fatal("session should be deleted")
|
|
}
|
|
|
|
ids, _ := database.GetChannelMemberIDs(ctx, sharedID)
|
|
if !slices.Equal(ids, []int64{peerSID}) {
|
|
t.Fatalf("#shared members: got %v, want [%d]", ids, peerSID)
|
|
}
|
|
|
|
_, err = database.GetChannelByName(ctx, "#alone")
|
|
if err == nil {
|
|
t.Fatal("expected the emptied channel to be deleted")
|
|
}
|
|
|
|
checkOneQuitQueued(t, database, peerClientID, "deleteme")
|
|
|
|
peerIDs, err = database.QuitSession(ctx, sid, "deleteme", body)
|
|
if err != nil || len(peerIDs) != 0 {
|
|
t.Fatalf("second quit: got %v, %v; want none", peerIDs, err)
|
|
}
|
|
|
|
checkOneQuitQueued(t, database, peerClientID, "deleteme")
|
|
}
|
|
|
|
// checkOneQuitQueued fails the test unless the queue of the
|
|
// client clientID holds one message, a QUIT from nick.
|
|
func checkOneQuitQueued(
|
|
t *testing.T,
|
|
database *db.Database,
|
|
clientID int64,
|
|
nick string,
|
|
) {
|
|
t.Helper()
|
|
|
|
msgs, _, err := database.PollMessages(t.Context(), clientID, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(msgs) != 1 || msgs[0].Command != "QUIT" || msgs[0].From != nick {
|
|
t.Fatalf("queue: got %v, want one QUIT from %s", msgs, nick)
|
|
}
|
|
}
|
|
|
|
// TestLogoutClientFailureKeepsClient checks that when the
|
|
// session of a logout's last client cannot be deleted,
|
|
// LogoutClient fails and the client stays logged in.
|
|
func TestLogoutClientFailureKeepsClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, clientID, token, err := database.CreateSession(
|
|
ctx, "stuck", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = database.GetDB().ExecContext(ctx,
|
|
`CREATE TRIGGER keep_sessions
|
|
BEFORE DELETE ON sessions
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'sessions are kept');
|
|
END`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = database.LogoutClient(ctx, sid, clientID, "stuck", nil)
|
|
if err == nil {
|
|
t.Fatal("logout succeeded although the session was kept")
|
|
}
|
|
|
|
_, _, nick, err := database.GetSessionByToken(ctx, token)
|
|
if err != nil || nick != "stuck" {
|
|
t.Fatalf("client gone after a failed logout: %q, %v", nick, err)
|
|
}
|
|
}
|
|
|
|
func TestChannelMembers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid1, _, _, err := database.CreateSession(ctx, "m1", "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sid2, _, _, err := database.CreateSession(ctx, "m2", "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chID, err := database.GetOrCreateChannel(
|
|
ctx, "#members",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
members, err := database.ChannelMembers(ctx, chID)
|
|
if err != nil || len(members) != 2 {
|
|
t.Fatalf(
|
|
"expected 2 members, got %d",
|
|
len(members),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGetSessionChannels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
sid, _, _ := createSessionWithChannels(
|
|
t, database, "multi", "#m1", "#m2",
|
|
)
|
|
|
|
channels, err :=
|
|
database.GetSessionChannels(
|
|
t.Context(), sid,
|
|
)
|
|
if err != nil || len(channels) != 2 {
|
|
t.Fatalf(
|
|
"expected 2 channels, got %d",
|
|
len(channels),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestEnqueueToClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
_, _, token, err := database.CreateSession(
|
|
ctx, "enqclient", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, clientID, _, err := database.GetSessionByToken(
|
|
ctx, token,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body := json.RawMessage(`["test"]`)
|
|
|
|
dbID, _, err := database.InsertMessage(
|
|
ctx, "PRIVMSG", "sender", "#ch", 0, 0, nil, body, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.EnqueueToClient(ctx, clientID, dbID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
const batchSize = 10
|
|
|
|
msgs, _, err := database.PollMessages(
|
|
ctx, clientID, 0, batchSize,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("expected 1, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestSetAndCheckSessionOper(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sessionID, _, _, err := database.CreateSession(
|
|
ctx, "opernick", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Initially not oper.
|
|
isOper, err := database.IsSessionOper(ctx, sessionID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if isOper {
|
|
t.Fatal("expected session not to be oper")
|
|
}
|
|
|
|
// Set oper.
|
|
err = database.SetSessionOper(ctx, sessionID, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
isOper, err = database.IsSessionOper(ctx, sessionID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !isOper {
|
|
t.Fatal("expected session to be oper")
|
|
}
|
|
|
|
// Unset oper.
|
|
err = database.SetSessionOper(ctx, sessionID, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
isOper, err = database.IsSessionOper(ctx, sessionID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if isOper {
|
|
t.Fatal("expected session not to be oper")
|
|
}
|
|
}
|
|
|
|
func TestGetLatestClientForSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sessionID, _, _, err := database.CreateSession(
|
|
ctx, "clientnick", "", "", "10.0.0.1",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientInfo, err := database.GetLatestClientForSession(
|
|
ctx, sessionID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if clientInfo.IP != "10.0.0.1" {
|
|
t.Fatalf(
|
|
"expected IP 10.0.0.1, got %s",
|
|
clientInfo.IP,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGetOperCount(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
// Create two sessions.
|
|
sid1, _, _, err := database.CreateSession(
|
|
ctx, "user1", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sid2, _, _, err := database.CreateSession(
|
|
ctx, "user2", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_ = sid2
|
|
|
|
// Initially zero opers.
|
|
count, err := database.GetOperCount(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if count != 0 {
|
|
t.Fatalf("expected 0 opers, got %d", count)
|
|
}
|
|
|
|
// Set one as oper.
|
|
err = database.SetSessionOper(ctx, sid1, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
count, err = database.GetOperCount(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if count != 1 {
|
|
t.Fatalf("expected 1 oper, got %d", count)
|
|
}
|
|
}
|
|
|
|
// --- Tier 2 Tests ---
|
|
|
|
func TestWildcardMatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const hostmask = "nick!user@host"
|
|
|
|
tests := []struct {
|
|
pattern string
|
|
input string
|
|
match bool
|
|
}{
|
|
{"*!*@*", hostmask, true},
|
|
{"*!*@*.example.com", "nick!user@foo.example.com", true},
|
|
{"*!*@*.example.com", "nick!user@other.net", false},
|
|
{"badnick!*@*", "badnick!user@host", true},
|
|
{"badnick!*@*", "goodnick!user@host", false},
|
|
{hostmask, hostmask, true},
|
|
{hostmask, "nick!user@other", false},
|
|
{"*", "anything", true},
|
|
{"?ick!*@*", hostmask, true},
|
|
{"?ick!*@*", "nn!user@host", false},
|
|
// Case-insensitive.
|
|
{"Nick!*@*", hostmask, true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := db.MatchBanMask(tc.pattern, tc.input)
|
|
if result != tc.match {
|
|
t.Errorf(
|
|
"MatchBanMask(%q, %q) = %v, want %v",
|
|
tc.pattern, tc.input, result, tc.match,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChannelBanCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// No bans initially.
|
|
bans, err := database.ListChannelBans(ctx, chID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(bans) != 0 {
|
|
t.Fatalf("expected 0 bans, got %d", len(bans))
|
|
}
|
|
|
|
// Add a ban.
|
|
err = database.AddChannelBan(
|
|
ctx, chID, "*!*@evil.com", "op",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bans, err = database.ListChannelBans(ctx, chID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(bans) != 1 {
|
|
t.Fatalf("expected 1 ban, got %d", len(bans))
|
|
}
|
|
|
|
if bans[0].Mask != "*!*@evil.com" {
|
|
t.Fatalf("wrong mask: %s", bans[0].Mask)
|
|
}
|
|
|
|
// Duplicate add is ignored (OR IGNORE).
|
|
err = database.AddChannelBan(
|
|
ctx, chID, "*!*@evil.com", "op2",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bans, _ = database.ListChannelBans(ctx, chID)
|
|
if len(bans) != 1 {
|
|
t.Fatalf("expected 1 ban after dup, got %d", len(bans))
|
|
}
|
|
|
|
// Remove ban.
|
|
err = database.RemoveChannelBan(
|
|
ctx, chID, "*!*@evil.com",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bans, _ = database.ListChannelBans(ctx, chID)
|
|
if len(bans) != 0 {
|
|
t.Fatalf("expected 0 bans after remove, got %d", len(bans))
|
|
}
|
|
}
|
|
|
|
func TestIsSessionBanned(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, _, _, err := database.CreateSession(
|
|
ctx, "victim", "victim", "evil.com", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#bantest")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Not banned initially.
|
|
banned, err := database.IsSessionBanned(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if banned {
|
|
t.Fatal("should not be banned initially")
|
|
}
|
|
|
|
// Add ban matching the hostmask.
|
|
err = database.AddChannelBan(
|
|
ctx, chID, "*!*@evil.com", "op",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
banned, err = database.IsSessionBanned(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !banned {
|
|
t.Fatal("should be banned")
|
|
}
|
|
}
|
|
|
|
func TestChannelInviteOnly(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#invite")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Default: not invite-only.
|
|
isIO, err := database.IsChannelInviteOnly(ctx, chID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if isIO {
|
|
t.Fatal("should not be invite-only by default")
|
|
}
|
|
|
|
// Set invite-only.
|
|
err = database.SetChannelInviteOnly(ctx, chID, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
isIO, _ = database.IsChannelInviteOnly(ctx, chID)
|
|
if !isIO {
|
|
t.Fatal("should be invite-only")
|
|
}
|
|
|
|
// Unset.
|
|
err = database.SetChannelInviteOnly(ctx, chID, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
isIO, _ = database.IsChannelInviteOnly(ctx, chID)
|
|
if isIO {
|
|
t.Fatal("should not be invite-only")
|
|
}
|
|
}
|
|
|
|
func TestChannelInviteCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, _, _, err := database.CreateSession(
|
|
ctx, "invited", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#inv")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// No invite initially.
|
|
has, err := database.HasChannelInvite(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if has {
|
|
t.Fatal("should not have invite")
|
|
}
|
|
|
|
// Add invite.
|
|
err = database.AddChannelInvite(ctx, chID, sid, "op")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
has, _ = database.HasChannelInvite(ctx, chID, sid)
|
|
if !has {
|
|
t.Fatal("should have invite")
|
|
}
|
|
}
|
|
|
|
func TestChannelSecret(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#secret")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Default: not secret.
|
|
isSec, err := database.IsChannelSecret(ctx, chID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if isSec {
|
|
t.Fatal("should not be secret by default")
|
|
}
|
|
|
|
err = database.SetChannelSecret(ctx, chID, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
isSec, _ = database.IsChannelSecret(ctx, chID)
|
|
if !isSec {
|
|
t.Fatal("should be secret")
|
|
}
|
|
}
|
|
|
|
// createTestSession is a helper to create a session and
|
|
// return only the session ID.
|
|
func createTestSession(
|
|
t *testing.T,
|
|
database *db.Database,
|
|
nick string,
|
|
) int64 {
|
|
t.Helper()
|
|
|
|
sid, _, _, err := database.CreateSession(
|
|
t.Context(), nick, "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("create session %s: %v", nick, err)
|
|
}
|
|
|
|
return sid
|
|
}
|
|
|
|
func TestSecretChannelFiltering(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
// Create two sessions.
|
|
sid1 := createTestSession(t, database, "member")
|
|
sid2 := createTestSession(t, database, "outsider")
|
|
|
|
// Create a secret channel.
|
|
chID, _ := database.GetOrCreateChannel(ctx, "#secret")
|
|
_ = database.SetChannelSecret(ctx, chID, true)
|
|
_ = database.JoinChannel(ctx, chID, sid1)
|
|
|
|
// Create a non-secret channel.
|
|
chID2, _ := database.GetOrCreateChannel(ctx, "#public")
|
|
_ = database.JoinChannel(ctx, chID2, sid1)
|
|
|
|
// Member should see both.
|
|
list, err := database.ListAllChannelsWithCountsFiltered(
|
|
ctx, sid1,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(list) != 2 {
|
|
t.Fatalf("member should see 2 channels, got %d", len(list))
|
|
}
|
|
|
|
// Outsider should only see public.
|
|
list, _ = database.ListAllChannelsWithCountsFiltered(
|
|
ctx, sid2,
|
|
)
|
|
if len(list) != 1 {
|
|
t.Fatalf("outsider should see 1 channel, got %d", len(list))
|
|
}
|
|
|
|
if list[0].Name != "#public" {
|
|
t.Fatalf("outsider should see #public, got %s", list[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestWhoisChannelFiltering(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid1 := createTestSession(t, database, "target")
|
|
sid2 := createTestSession(t, database, "querier")
|
|
|
|
// Create secret channel, target joins it.
|
|
chID, _ := database.GetOrCreateChannel(ctx, "#hidden")
|
|
_ = database.SetChannelSecret(ctx, chID, true)
|
|
_ = database.JoinChannel(ctx, chID, sid1)
|
|
|
|
// Querier (non-member) should not see the channel.
|
|
channels, err := database.GetSessionChannelsFiltered(
|
|
ctx, sid1, sid2,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(channels) != 0 {
|
|
t.Fatalf(
|
|
"querier should see 0 channels, got %d",
|
|
len(channels),
|
|
)
|
|
}
|
|
|
|
// Target querying self should see it.
|
|
channels, _ = database.GetSessionChannelsFiltered(
|
|
ctx, sid1, sid1,
|
|
)
|
|
if len(channels) != 1 {
|
|
t.Fatalf(
|
|
"self-query should see 1 channel, got %d",
|
|
len(channels),
|
|
)
|
|
}
|
|
}
|
|
|
|
//nolint:dupl // structurally similar to TestChannelUserLimit
|
|
func TestChannelKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#keyed")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Default: no key.
|
|
key, err := database.GetChannelKey(ctx, chID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if key != "" {
|
|
t.Fatalf("expected empty key, got %q", key)
|
|
}
|
|
|
|
// Set key.
|
|
err = database.SetChannelKey(ctx, chID, "secret123")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
key, _ = database.GetChannelKey(ctx, chID)
|
|
if key != "secret123" {
|
|
t.Fatalf("expected secret123, got %q", key)
|
|
}
|
|
|
|
// Clear key.
|
|
err = database.SetChannelKey(ctx, chID, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
key, _ = database.GetChannelKey(ctx, chID)
|
|
if key != "" {
|
|
t.Fatalf("expected empty key, got %q", key)
|
|
}
|
|
}
|
|
|
|
//nolint:dupl // structurally similar to TestChannelKey
|
|
func TestChannelUserLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(ctx, "#limited")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Default: no limit.
|
|
limit, err := database.GetChannelUserLimit(ctx, chID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if limit != 0 {
|
|
t.Fatalf("expected 0 limit, got %d", limit)
|
|
}
|
|
|
|
// Set limit.
|
|
err = database.SetChannelUserLimit(ctx, chID, 50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
limit, _ = database.GetChannelUserLimit(ctx, chID)
|
|
if limit != 50 {
|
|
t.Fatalf("expected 50, got %d", limit)
|
|
}
|
|
|
|
// Clear limit.
|
|
err = database.SetChannelUserLimit(ctx, chID, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
limit, _ = database.GetChannelUserLimit(ctx, chID)
|
|
if limit != 0 {
|
|
t.Fatalf("expected 0, got %d", limit)
|
|
}
|
|
}
|
|
|
|
// TestSetSessionUserModesIsAtomic makes the is_oper write
|
|
// fail after the is_wallops write has run, and checks that
|
|
// the wallops change was rolled back.
|
|
func TestSetSessionUserModesIsAtomic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sessionID, _, _, err := database.CreateSession(
|
|
ctx, "alice", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.ExecForTest(ctx,
|
|
`CREATE TRIGGER reject_oper
|
|
BEFORE UPDATE OF is_oper ON sessions
|
|
BEGIN SELECT RAISE(ABORT, 'oper write rejected');
|
|
END`,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wallops := true
|
|
oper := false
|
|
|
|
err = database.SetSessionUserModes(
|
|
ctx, sessionID, &wallops, &oper,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected the rejected oper write to fail")
|
|
}
|
|
|
|
gotWallops, err := database.IsSessionWallops(
|
|
ctx, sessionID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if gotWallops {
|
|
t.Error("wallops was stored although the change failed")
|
|
}
|
|
}
|
|
|
|
// TestSetSessionUserModesAppliesBoth checks that both flags
|
|
// are written, and that a nil flag is left as it is.
|
|
func TestSetSessionUserModesAppliesBoth(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sessionID, _, _, err := database.CreateSession(
|
|
ctx, "alice", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.SetSessionOper(ctx, sessionID, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wallops := true
|
|
oper := false
|
|
|
|
err = database.SetSessionUserModes(
|
|
ctx, sessionID, &wallops, &oper,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gotWallops, err := database.IsSessionWallops(
|
|
ctx, sessionID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gotOper, err := database.IsSessionOper(ctx, sessionID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !gotWallops || gotOper {
|
|
t.Errorf(
|
|
"want wallops=true oper=false, got %v/%v",
|
|
gotWallops, gotOper,
|
|
)
|
|
}
|
|
|
|
err = database.SetSessionUserModes(
|
|
ctx, sessionID, nil, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gotWallops, err = database.IsSessionWallops(
|
|
ctx, sessionID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !gotWallops {
|
|
t.Error("a nil wallops flag cleared wallops")
|
|
}
|
|
}
|