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>
This commit was merged in pull request #96.
This commit is contained in:
@@ -10,7 +10,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxtest"
|
||||
@@ -55,9 +58,10 @@ func newTestEnv(t *testing.T) *testEnv {
|
||||
app := fxtest.New(t,
|
||||
fx.Provide(
|
||||
func() *globals.Globals {
|
||||
return &globals.Globals{ //nolint:exhaustruct
|
||||
Appname: "neoirc-test",
|
||||
Version: "test",
|
||||
return &globals.Globals{
|
||||
Appname: "neoirc-test",
|
||||
Version: "test",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
},
|
||||
logger.New,
|
||||
@@ -363,3 +367,249 @@ func TestSendChannelMessage_Moderated(t *testing.T) {
|
||||
t.Errorf("operator should be able to send in moderated channel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryUserMode(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid := createSession(ctx, t, env.db, "alice")
|
||||
|
||||
modes, err := env.svc.QueryUserMode(ctx, sid)
|
||||
if err != nil {
|
||||
t.Fatalf("query user mode: %v", err)
|
||||
}
|
||||
|
||||
if modes != "+" {
|
||||
t.Errorf("expected +, got %s", modes)
|
||||
}
|
||||
|
||||
setUserModes(ctx, t, env.db, sid, userModes{wallops: true})
|
||||
|
||||
modes, err = env.svc.QueryUserMode(ctx, sid)
|
||||
if err != nil {
|
||||
t.Fatalf("query user mode: %v", err)
|
||||
}
|
||||
|
||||
if modes != "+w" {
|
||||
t.Errorf("expected +w, got %s", modes)
|
||||
}
|
||||
|
||||
setUserModes(
|
||||
ctx, t, env.db, sid, userModes{oper: true, wallops: true},
|
||||
)
|
||||
|
||||
modes, err = env.svc.QueryUserMode(ctx, sid)
|
||||
if err != nil {
|
||||
t.Fatalf("query user mode: %v", err)
|
||||
}
|
||||
|
||||
if modes != "+ow" {
|
||||
t.Errorf("expected +ow, got %s", modes)
|
||||
}
|
||||
|
||||
// A flag that cannot be read is an error, not a flag
|
||||
// that is unset.
|
||||
cancelled, cancel := context.WithCancel(ctx)
|
||||
cancel()
|
||||
|
||||
_, err = env.svc.QueryUserMode(cancelled, sid)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("expected the read's error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserhostReply(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
createSession(ctx, t, env.db, "alice")
|
||||
bobID := createSession(ctx, t, env.db, "bob")
|
||||
|
||||
// No username or hostname, and an operator.
|
||||
operID, _, _, err := env.db.CreateSession(
|
||||
ctx, "oper", "", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
setUserModes(ctx, t, env.db, operID, userModes{oper: true})
|
||||
|
||||
_, err = env.svc.SetAway(ctx, bobID, "gone fishing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reply, err := env.svc.UserhostReply(ctx, []string{
|
||||
"alice", "bob", "nobody", "oper",
|
||||
}, "srv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := "alice=+alice@localhost bob=-bob@localhost " +
|
||||
"oper*=+oper@srv"
|
||||
if reply != want {
|
||||
t.Errorf("want %q, got %q", want, reply)
|
||||
}
|
||||
|
||||
reply, err = env.svc.UserhostReply(
|
||||
ctx, slices.Repeat([]string{"bob"}, 6), "srv",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strings.Count(reply, "bob=") != 5 {
|
||||
t.Errorf("want 5 entries for 6 nicks, got %q", reply)
|
||||
}
|
||||
|
||||
// A nick that cannot be looked up is an error, not a nick
|
||||
// with no session.
|
||||
cancelled, cancel := context.WithCancel(ctx)
|
||||
cancel()
|
||||
|
||||
_, err = env.svc.UserhostReply(cancelled, []string{"alice"}, "srv")
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("want the lookup's error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// userModes is the stored state of a session's user mode
|
||||
// flags.
|
||||
type userModes struct {
|
||||
oper bool
|
||||
wallops bool
|
||||
}
|
||||
|
||||
// applyUserModeTest is one mode string, the flags stored
|
||||
// before it is applied, and the expected outcome. An empty
|
||||
// wantModes means the string is rejected with
|
||||
// ERR_UMODEUNKNOWNFLAG, and then after must equal before.
|
||||
type applyUserModeTest struct {
|
||||
modeStr string
|
||||
before userModes
|
||||
wantModes string
|
||||
after userModes
|
||||
}
|
||||
|
||||
func applyUserModeTests() []applyUserModeTest {
|
||||
none := userModes{}
|
||||
oper := userModes{oper: true}
|
||||
wallops := userModes{wallops: true}
|
||||
both := userModes{oper: true, wallops: true}
|
||||
|
||||
return []applyUserModeTest{
|
||||
{"+w", none, "+w", wallops},
|
||||
{"-w", wallops, "+", none},
|
||||
{"-o", oper, "+", none},
|
||||
{"-wo", both, "+", none},
|
||||
{"+w-o", oper, "+w", wallops},
|
||||
{"-w+w", none, "+w", wallops},
|
||||
{"+w-w", wallops, "+", none},
|
||||
{"-w+o", both, "", both},
|
||||
{"+o-w+w", both, "", both},
|
||||
{"+wo", none, "", none},
|
||||
{"+wz", none, "", none},
|
||||
{"+z", none, "", none},
|
||||
{"-x+y", none, "", none},
|
||||
{"+y-x", none, "", none},
|
||||
{"w", none, "", none},
|
||||
{"xw", wallops, "", wallops},
|
||||
{"", none, "", none},
|
||||
{"+", none, "", none},
|
||||
{"-", none, "", none},
|
||||
{"+-+", none, "", none},
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyUserMode(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
|
||||
for i, test := range applyUserModeTests() {
|
||||
t.Run(test.modeStr, func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
sid := createSession(
|
||||
ctx, t, env.db, fmt.Sprintf("user%d", i),
|
||||
)
|
||||
setUserModes(ctx, t, env.db, sid, test.before)
|
||||
|
||||
modes, err := env.svc.ApplyUserMode(
|
||||
ctx, sid, test.modeStr,
|
||||
)
|
||||
checkApplyUserModeResult(t, test.wantModes, modes, err)
|
||||
|
||||
got := getUserModes(ctx, t, env.db, sid)
|
||||
if got != test.after {
|
||||
t.Errorf(
|
||||
"stored modes: want %+v, got %+v",
|
||||
test.after, got,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkApplyUserModeResult(
|
||||
t *testing.T,
|
||||
wantModes, modes string,
|
||||
err error,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
if wantModes == "" {
|
||||
var ircErr *service.IRCError
|
||||
if !errors.As(err, &ircErr) ||
|
||||
ircErr.Code != irc.ErrUmodeUnknownFlag {
|
||||
t.Fatalf("want ERR_UMODEUNKNOWNFLAG, got %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if modes != wantModes {
|
||||
t.Errorf("modes: want %q, got %q", wantModes, modes)
|
||||
}
|
||||
}
|
||||
|
||||
func setUserModes(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
database *db.Database,
|
||||
sid int64,
|
||||
modes userModes,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
err := database.SetSessionUserModes(
|
||||
ctx, sid, &modes.wallops, &modes.oper,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("set user modes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getUserModes(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
database *db.Database,
|
||||
sid int64,
|
||||
) userModes {
|
||||
t.Helper()
|
||||
|
||||
oper, err := database.IsSessionOper(ctx, sid)
|
||||
if err != nil {
|
||||
t.Fatalf("read oper: %v", err)
|
||||
}
|
||||
|
||||
wallops, err := database.IsSessionWallops(ctx, sid)
|
||||
if err != nil {
|
||||
t.Fatalf("read wallops: %v", err)
|
||||
}
|
||||
|
||||
return userModes{oper: oper, wallops: wallops}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user