Some checks failed
check / check (push) Failing after 16s
db: TestSetSessionUserModesIsAtomic installs a trigger that rejects the is_oper UPDATE after the is_wallops UPDATE has run, proving the '+w-o' partial-failure the old independent-UPDATE loop exhibited is gone. ircserver: TestDisconnectDoesNotBlockOnUnresponsiveVictim drives Disconnect against a net.Pipe victim that never reads and requires the call to return promptly; verified to fail against the synchronous implementation. Its counterpart asserts the notification is still delivered and the socket still closed.
1615 lines
29 KiB
Go
1615 lines
29 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", "", "", "",
|
|
)
|
|
_ = sid2
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// 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()
|
|
|
|
tests := []struct {
|
|
pattern string
|
|
input string
|
|
match bool
|
|
}{
|
|
{"*!*@*", "nick!user@host", 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},
|
|
{"nick!user@host", "nick!user@host", true},
|
|
{"nick!user@host", "nick!user@other", false},
|
|
{"*", "anything", true},
|
|
{"?ick!*@*", "nick!user@host", true},
|
|
{"?ick!*@*", "nn!user@host", false},
|
|
// Case-insensitive.
|
|
{"Nick!*@*", "nick!user@host", 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 proves that a multi-mode
|
|
// change is all-or-nothing. A trigger makes the is_oper
|
|
// UPDATE fail after the is_wallops UPDATE has already run,
|
|
// which is exactly the "+w-o" partial-failure the previous
|
|
// implementation exhibited: it issued the two UPDATEs
|
|
// independently, so +w persisted while the caller reported
|
|
// total failure.
|
|
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 persisted despite the transaction " +
|
|
"failing; the apply stage is not atomic",
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestSetSessionUserModesAppliesBoth is the success-path
|
|
// counterpart: when nothing fails, both flags are written,
|
|
// and a nil pointer leaves that flag untouched.
|
|
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)
|
|
}
|
|
|
|
if err := database.SetSessionOper(
|
|
ctx, sessionID, true,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wallops := true
|
|
oper := false
|
|
|
|
if err := database.SetSessionUserModes(
|
|
ctx, sessionID, &wallops, &oper,
|
|
); 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,
|
|
)
|
|
}
|
|
|
|
// A nil pointer must leave the stored value alone.
|
|
if err := database.SetSessionUserModes(
|
|
ctx, sessionID, nil, nil,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gotWallops, err = database.IsSessionWallops(
|
|
ctx, sessionID,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !gotWallops {
|
|
t.Error("nil pointers must not clear wallops")
|
|
}
|
|
}
|