check / check (push) Waiting to run
USERHOST, VERSION, ADMIN, INFO, TIME, KILL and WALLOPS work on the HTTP API and on the IRC listener. The USERHOST reply, the VERSION, ADMIN and INFO text, KILL and WALLOPS are each one service function that both transports call. User mode +w selects who receives WALLOPS. MODE on your own nick queries or changes your user modes through one parser on both transports: the whole mode string is checked before anything is stored, and both flags are stored in one transaction. KILL closes the victim's IRC connection after sending it KILL and ERROR, without the operator waiting on it. The sessions table gains is_wallops in 001_initial.sql, as the pre-1.0 rule in REPO_POLICIES.md asks. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
1610 lines
28 KiB
Go
1610 lines
28 KiB
Go
package db_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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.PartChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ids, _ = database.GetChannelMemberIDs(ctx, chID)
|
|
if len(ids) != 0 {
|
|
t.Fatal("expected empty channel")
|
|
}
|
|
}
|
|
|
|
func TestDeleteChannelIfEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
chID, err := database.GetOrCreateChannel(
|
|
ctx, "#empty",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sid, _, _, err := database.CreateSession(ctx, "temp", "", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.PartChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.DeleteChannelIfEmpty(ctx, chID)
|
|
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", 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", nil, body, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.EnqueueToSession(ctx, sid, dbID)
|
|
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()
|
|
|
|
const msgCount = 10
|
|
|
|
for range msgCount {
|
|
_, _, err := database.InsertMessage(
|
|
ctx, "PRIVMSG", "user", "#hist",
|
|
nil, json.RawMessage(`["msg"]`), nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
const histLimit = 5
|
|
|
|
msgs, err := database.GetHistory(
|
|
ctx, "#hist", 0, 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")
|
|
}
|
|
}
|
|
|
|
func TestDeleteSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
database := setupTestDB(t)
|
|
ctx := t.Context()
|
|
|
|
sid, _, _, err := database.CreateSession(
|
|
ctx, "deleteme", "", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chID, err := database.GetOrCreateChannel(
|
|
ctx, "#delchan",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.JoinChannel(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.DeleteSession(ctx, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = database.GetSessionByNick(ctx, "deleteme")
|
|
if err == nil {
|
|
t.Fatal("session should be deleted")
|
|
}
|
|
|
|
ids, _ := database.GetChannelMemberIDs(ctx, chID)
|
|
if len(ids) != 0 {
|
|
t.Fatal("expected no members after deletion")
|
|
}
|
|
}
|
|
|
|
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", 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")
|
|
}
|
|
|
|
// Clear invite.
|
|
err = database.ClearChannelInvite(ctx, chID, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
has, _ = database.HasChannelInvite(ctx, chID, sid)
|
|
if has {
|
|
t.Fatal("invite should be cleared")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|