Compare commits
2 Commits
17479c4f44
...
feature/87
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abe0cc2c30 | ||
|
|
327ff37059 |
@@ -2307,8 +2307,8 @@ IRC_LISTEN_ADDR=
|
||||
| Connection | `NICK`, `USER`, `PASS`, `QUIT`, `PING`/`PONG`, `CAP` |
|
||||
| Channels | `JOIN`, `PART`, `MODE`, `TOPIC`, `NAMES`, `LIST`, `KICK`, `INVITE` |
|
||||
| Messaging | `PRIVMSG`, `NOTICE` |
|
||||
| Info | `WHO`, `WHOIS`, `LUSERS`, `MOTD`, `AWAY` |
|
||||
| Operator | `OPER` (requires `NEOIRC_OPER_NAME` and `NEOIRC_OPER_PASSWORD`) |
|
||||
| Info | `WHO`, `WHOIS`, `LUSERS`, `MOTD`, `AWAY`, `USERHOST`, `VERSION`, `ADMIN`, `INFO`, `TIME` |
|
||||
| Operator | `OPER`, `KILL`, `WALLOPS` (requires `NEOIRC_OPER_NAME` and `NEOIRC_OPER_PASSWORD`) |
|
||||
|
||||
### Protocol Details
|
||||
|
||||
@@ -2820,6 +2820,10 @@ guess is borne by the server (bcrypt), not the client.
|
||||
login from additional devices via `POST /api/v1/login`
|
||||
- [x] **Cookie-based auth** — HttpOnly cookies replace Bearer tokens for
|
||||
all API authentication
|
||||
- [x] **Tier 3 utility commands** — USERHOST (302), VERSION (351), ADMIN
|
||||
(256–259), INFO (371/374), TIME (391), KILL (oper-only forced
|
||||
disconnect), WALLOPS (oper-only broadcast to +w users)
|
||||
- [x] **User mode +w** — wallops usermode via `MODE nick +w/-w`
|
||||
|
||||
### Future (1.0+)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
@@ -331,18 +331,12 @@ func (hdlr *Handlers) handleKill(
|
||||
}
|
||||
|
||||
lines := bodyLines()
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrNeedMoreParams, nick,
|
||||
[]string{irc.CmdKill},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
var targetNick string
|
||||
if len(lines) > 0 {
|
||||
targetNick = strings.TrimSpace(lines[0])
|
||||
}
|
||||
|
||||
targetNick := strings.TrimSpace(lines[0])
|
||||
if targetNick == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
@@ -383,8 +377,11 @@ func (hdlr *Handlers) handleKill(
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.executeKillUser(
|
||||
request, targetSID, targetNick, nick, reason,
|
||||
quitReason := "Killed (" + nick + " (" + reason + "))"
|
||||
|
||||
hdlr.svc.BroadcastQuit(
|
||||
request.Context(), targetSID,
|
||||
targetNick, quitReason,
|
||||
)
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
@@ -392,71 +389,6 @@ func (hdlr *Handlers) handleKill(
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// executeKillUser forcibly disconnects a user: broadcasts
|
||||
// QUIT to their channels, parts all channels, and deletes
|
||||
// the session.
|
||||
func (hdlr *Handlers) executeKillUser(
|
||||
request *http.Request,
|
||||
targetSID int64,
|
||||
targetNick, killerNick, reason string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
quitMsg := "Killed (" + killerNick + " (" + reason + "))"
|
||||
|
||||
quitBody, err := json.Marshal([]string{quitMsg})
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"marshal kill quit body", "error", err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channels, _ := hdlr.params.Database.
|
||||
GetSessionChannels(ctx, targetSID)
|
||||
|
||||
notified := map[int64]bool{}
|
||||
|
||||
var dbID int64
|
||||
|
||||
if len(channels) > 0 {
|
||||
dbID, _, _ = hdlr.params.Database.InsertMessage(
|
||||
ctx, irc.CmdQuit, targetNick, "",
|
||||
nil, json.RawMessage(quitBody), nil,
|
||||
)
|
||||
}
|
||||
|
||||
for _, chanInfo := range channels {
|
||||
memberIDs, _ := hdlr.params.Database.
|
||||
GetChannelMemberIDs(ctx, chanInfo.ID)
|
||||
|
||||
for _, mid := range memberIDs {
|
||||
if mid != targetSID && !notified[mid] {
|
||||
notified[mid] = true
|
||||
|
||||
_ = hdlr.params.Database.EnqueueToSession(
|
||||
ctx, mid, dbID,
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(mid)
|
||||
}
|
||||
}
|
||||
|
||||
_ = hdlr.params.Database.PartChannel(
|
||||
ctx, chanInfo.ID, targetSID,
|
||||
)
|
||||
|
||||
_ = hdlr.params.Database.DeleteChannelIfEmpty(
|
||||
ctx, chanInfo.ID,
|
||||
)
|
||||
}
|
||||
|
||||
_ = hdlr.params.Database.DeleteSession(
|
||||
ctx, targetSID,
|
||||
)
|
||||
}
|
||||
|
||||
// handleWallops handles the WALLOPS command.
|
||||
// Broadcasts a message to all users with +w usermode
|
||||
// (oper only).
|
||||
@@ -539,7 +471,10 @@ func (hdlr *Handlers) handleWallops(
|
||||
}
|
||||
|
||||
// handleUserMode handles user mode queries and changes
|
||||
// (e.g., MODE nick, MODE nick +w).
|
||||
// (e.g., MODE nick, MODE nick +w). Delegates to the
|
||||
// shared service.ApplyUserMode / service.QueryUserMode so
|
||||
// that mode string processing is identical for both the
|
||||
// HTTP API and IRC wire protocol.
|
||||
func (hdlr *Handlers) handleUserMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
@@ -564,86 +499,31 @@ func (hdlr *Handlers) handleUserMode(
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.applyUserModeChange(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, lines[0],
|
||||
newModes, err := hdlr.svc.ApplyUserMode(
|
||||
ctx, sessionID, lines[0],
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Mode query — build the current mode string.
|
||||
modeStr := hdlr.buildUserModeString(ctx, sessionID)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplUmodeIs, nick, nil,
|
||||
modeStr,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// buildUserModeString constructs the mode string for a
|
||||
// user (e.g., "+ow" for oper+wallops).
|
||||
func (hdlr *Handlers) buildUserModeString(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
) string {
|
||||
modes := "+"
|
||||
|
||||
isOper, err := hdlr.params.Database.IsSessionOper(
|
||||
ctx, sessionID,
|
||||
)
|
||||
if err == nil && isOper {
|
||||
modes += "o"
|
||||
}
|
||||
|
||||
isWallops, err := hdlr.params.Database.IsSessionWallops(
|
||||
ctx, sessionID,
|
||||
)
|
||||
if err == nil && isWallops {
|
||||
modes += "w"
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
|
||||
// applyUserModeChange applies a user mode change string
|
||||
// (e.g., "+w", "-w").
|
||||
func (hdlr *Handlers) applyUserModeChange(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, modeStr string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
if len(modeStr) < 2 { //nolint:mnd // +/- and mode char
|
||||
if err != nil {
|
||||
var ircErr *service.IRCError
|
||||
if errors.As(err, &ircErr) {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrUmodeUnknownFlag, nick, nil,
|
||||
"Unknown MODE flag",
|
||||
writer, request,
|
||||
clientID, sessionID,
|
||||
ircErr.Code, nick, ircErr.Params,
|
||||
ircErr.Message,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
adding := modeStr[0] == '+'
|
||||
modeChar := modeStr[1:]
|
||||
|
||||
applied, err := hdlr.applyModeChar(
|
||||
ctx, writer, request,
|
||||
sessionID, clientID, nick,
|
||||
modeChar, adding,
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
if err != nil || !applied {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newModes := hdlr.buildUserModeString(ctx, sessionID)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplUmodeIs, nick, nil,
|
||||
newModes,
|
||||
@@ -653,75 +533,19 @@ func (hdlr *Handlers) applyUserModeChange(
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// applyModeChar applies a single user mode character.
|
||||
// Returns (applied, error).
|
||||
func (hdlr *Handlers) applyModeChar(
|
||||
ctx context.Context,
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, modeChar string,
|
||||
adding bool,
|
||||
) (bool, error) {
|
||||
switch modeChar {
|
||||
case "w":
|
||||
err := hdlr.params.Database.SetSessionWallops(
|
||||
ctx, sessionID, adding,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"set wallops mode failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
// Mode query — delegate to shared service.
|
||||
modeStr := hdlr.svc.QueryUserMode(ctx, sessionID)
|
||||
|
||||
return false, fmt.Errorf(
|
||||
"set wallops: %w", err,
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplUmodeIs, nick, nil,
|
||||
modeStr,
|
||||
)
|
||||
}
|
||||
case "o":
|
||||
// +o cannot be set via MODE, only via OPER.
|
||||
if adding {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrUmodeUnknownFlag, nick, nil,
|
||||
"Unknown MODE flag",
|
||||
)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err := hdlr.params.Database.SetSessionOper(
|
||||
ctx, sessionID, false,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"clear oper mode failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return false, fmt.Errorf(
|
||||
"clear oper: %w", err,
|
||||
)
|
||||
}
|
||||
default:
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrUmodeUnknownFlag, nick, nil,
|
||||
"Unknown MODE flag",
|
||||
)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,29 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/service"
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
// versionString returns the server version for IRC
|
||||
// responses, falling back to "neoirc-dev" when globals
|
||||
// are not set (e.g. during tests).
|
||||
func versionString() string {
|
||||
name := globals.Appname
|
||||
ver := globals.Version
|
||||
|
||||
if name == "" {
|
||||
name = "neoirc"
|
||||
}
|
||||
|
||||
if ver == "" {
|
||||
ver = "dev"
|
||||
}
|
||||
|
||||
return name + "-" + ver
|
||||
}
|
||||
|
||||
// sendIRCError maps a service.IRCError to an IRC numeric
|
||||
// reply on the wire.
|
||||
func (c *Conn) sendIRCError(err error) {
|
||||
@@ -711,17 +730,23 @@ func (c *Conn) handleUserMode(
|
||||
|
||||
// Mode query (no mode string).
|
||||
if len(msg.Params) < 2 { //nolint:mnd
|
||||
c.sendNumeric(
|
||||
irc.RplUmodeIs,
|
||||
c.buildUmodeString(ctx),
|
||||
)
|
||||
modes := c.svc.QueryUserMode(ctx, c.sessionID)
|
||||
c.sendNumeric(irc.RplUmodeIs, modes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
modeStr := msg.Params[1]
|
||||
newModes, err := c.svc.ApplyUserMode(
|
||||
ctx, c.sessionID, msg.Params[1],
|
||||
)
|
||||
if err != nil {
|
||||
var ircErr *service.IRCError
|
||||
if errors.As(err, &ircErr) {
|
||||
c.sendNumeric(ircErr.Code, ircErr.Message)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(modeStr) < 2 { //nolint:mnd
|
||||
c.sendNumeric(
|
||||
irc.ErrUmodeUnknownFlag,
|
||||
"Unknown MODE flag",
|
||||
@@ -730,64 +755,7 @@ func (c *Conn) handleUserMode(
|
||||
return
|
||||
}
|
||||
|
||||
adding := modeStr[0] == '+'
|
||||
|
||||
for _, ch := range modeStr[1:] {
|
||||
switch ch {
|
||||
case 'w':
|
||||
_ = c.database.SetSessionWallops(
|
||||
ctx, c.sessionID, adding,
|
||||
)
|
||||
case 'o':
|
||||
if adding {
|
||||
c.sendNumeric(
|
||||
irc.ErrUmodeUnknownFlag,
|
||||
"Unknown MODE flag",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.database.SetSessionOper(
|
||||
ctx, c.sessionID, false,
|
||||
)
|
||||
default:
|
||||
c.sendNumeric(
|
||||
irc.ErrUmodeUnknownFlag,
|
||||
"Unknown MODE flag",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplUmodeIs,
|
||||
c.buildUmodeString(ctx),
|
||||
)
|
||||
}
|
||||
|
||||
// buildUmodeString returns the current user mode string.
|
||||
func (c *Conn) buildUmodeString(
|
||||
ctx context.Context,
|
||||
) string {
|
||||
modes := "+"
|
||||
|
||||
isOper, err := c.database.IsSessionOper(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err == nil && isOper {
|
||||
modes += "o"
|
||||
}
|
||||
|
||||
isWallops, err := c.database.IsSessionWallops(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err == nil && isWallops {
|
||||
modes += "w"
|
||||
}
|
||||
|
||||
return modes
|
||||
c.sendNumeric(irc.RplUmodeIs, newModes)
|
||||
}
|
||||
|
||||
// handleNames replies with channel member list.
|
||||
@@ -1384,7 +1352,7 @@ func (c *Conn) handleUserhost(
|
||||
func (c *Conn) handleVersion(ctx context.Context) {
|
||||
_ = ctx
|
||||
|
||||
version := "neoirc-0.1"
|
||||
version := versionString()
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplVersion,
|
||||
@@ -1426,7 +1394,7 @@ func (c *Conn) handleInfo(ctx context.Context) {
|
||||
|
||||
infoLines := []string{
|
||||
"neoirc — IRC semantics over HTTP",
|
||||
"Version: neoirc-0.1",
|
||||
"Version: " + versionString(),
|
||||
"Written in Go",
|
||||
}
|
||||
|
||||
|
||||
@@ -791,6 +791,109 @@ func (s *Service) QueryChannelMode(
|
||||
return modes + modeParams
|
||||
}
|
||||
|
||||
// QueryUserMode returns the current user mode string for
|
||||
// the given session (e.g. "+ow", "+w", "+").
|
||||
func (s *Service) QueryUserMode(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
) string {
|
||||
modes := "+"
|
||||
|
||||
isOper, err := s.db.IsSessionOper(ctx, sessionID)
|
||||
if err == nil && isOper {
|
||||
modes += "o"
|
||||
}
|
||||
|
||||
isWallops, err := s.db.IsSessionWallops(
|
||||
ctx, sessionID,
|
||||
)
|
||||
if err == nil && isWallops {
|
||||
modes += "w"
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
|
||||
// ApplyUserMode parses a mode string character by
|
||||
// character (e.g. "+wo", "-w") and applies each mode
|
||||
// change to the session. Returns the resulting mode string
|
||||
// after all changes, or an IRCError on failure.
|
||||
func (s *Service) ApplyUserMode(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
modeStr string,
|
||||
) (string, error) {
|
||||
if len(modeStr) < 2 { //nolint:mnd // +/- prefix + ≥1 char
|
||||
return "", &IRCError{
|
||||
Code: irc.ErrUmodeUnknownFlag,
|
||||
Params: nil,
|
||||
Message: "Unknown MODE flag",
|
||||
}
|
||||
}
|
||||
|
||||
adding := modeStr[0] == '+'
|
||||
|
||||
for _, ch := range modeStr[1:] {
|
||||
if err := s.applySingleUserMode(
|
||||
ctx, sessionID, ch, adding,
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return s.QueryUserMode(ctx, sessionID), nil
|
||||
}
|
||||
|
||||
// applySingleUserMode applies one user mode character.
|
||||
func (s *Service) applySingleUserMode(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
modeChar rune,
|
||||
adding bool,
|
||||
) error {
|
||||
switch modeChar {
|
||||
case 'w':
|
||||
err := s.db.SetSessionWallops(
|
||||
ctx, sessionID, adding,
|
||||
)
|
||||
if err != nil {
|
||||
s.log.Error(
|
||||
"set wallops mode failed", "error", err,
|
||||
)
|
||||
|
||||
return fmt.Errorf("set wallops: %w", err)
|
||||
}
|
||||
case 'o':
|
||||
// +o cannot be set via MODE; use OPER command.
|
||||
if adding {
|
||||
return &IRCError{
|
||||
Code: irc.ErrUmodeUnknownFlag,
|
||||
Params: nil,
|
||||
Message: "Unknown MODE flag",
|
||||
}
|
||||
}
|
||||
|
||||
err := s.db.SetSessionOper(
|
||||
ctx, sessionID, false,
|
||||
)
|
||||
if err != nil {
|
||||
s.log.Error(
|
||||
"clear oper mode failed", "error", err,
|
||||
)
|
||||
|
||||
return fmt.Errorf("clear oper: %w", err)
|
||||
}
|
||||
default:
|
||||
return &IRCError{
|
||||
Code: irc.ErrUmodeUnknownFlag,
|
||||
Params: nil,
|
||||
Message: "Unknown MODE flag",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// broadcastNickChange notifies channel peers of a nick
|
||||
// change.
|
||||
func (s *Service) broadcastNickChange(
|
||||
|
||||
@@ -363,3 +363,166 @@ 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")
|
||||
|
||||
// Fresh session has no modes.
|
||||
modes := env.svc.QueryUserMode(ctx, sid)
|
||||
if modes != "+" {
|
||||
t.Errorf("expected +, got %s", modes)
|
||||
}
|
||||
|
||||
// Set wallops.
|
||||
_ = env.db.SetSessionWallops(ctx, sid, true)
|
||||
|
||||
modes = env.svc.QueryUserMode(ctx, sid)
|
||||
if modes != "+w" {
|
||||
t.Errorf("expected +w, got %s", modes)
|
||||
}
|
||||
|
||||
// Set oper.
|
||||
_ = env.db.SetSessionOper(ctx, sid, true)
|
||||
|
||||
modes = env.svc.QueryUserMode(ctx, sid)
|
||||
if modes != "+ow" {
|
||||
t.Errorf("expected +ow, got %s", modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyUserModeSingleChar(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid := createSession(ctx, t, env.db, "alice")
|
||||
|
||||
// Apply +w.
|
||||
result, err := env.svc.ApplyUserMode(ctx, sid, "+w")
|
||||
if err != nil {
|
||||
t.Fatalf("apply +w: %v", err)
|
||||
}
|
||||
|
||||
if result != "+w" {
|
||||
t.Errorf("expected +w, got %s", result)
|
||||
}
|
||||
|
||||
// Apply -w.
|
||||
result, err = env.svc.ApplyUserMode(ctx, sid, "-w")
|
||||
if err != nil {
|
||||
t.Fatalf("apply -w: %v", err)
|
||||
}
|
||||
|
||||
if result != "+" {
|
||||
t.Errorf("expected +, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyUserModeMultiChar(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid := createSession(ctx, t, env.db, "alice")
|
||||
|
||||
// Set oper first so we can test +wo (w applied, o
|
||||
// rejected because +o is not allowed via MODE).
|
||||
_ = env.db.SetSessionOper(ctx, sid, true)
|
||||
|
||||
// Apply +w alone should work.
|
||||
result, err := env.svc.ApplyUserMode(ctx, sid, "+w")
|
||||
if err != nil {
|
||||
t.Fatalf("apply +w: %v", err)
|
||||
}
|
||||
|
||||
if result != "+ow" {
|
||||
t.Errorf("expected +ow, got %s", result)
|
||||
}
|
||||
|
||||
// Reset wallops.
|
||||
_ = env.db.SetSessionWallops(ctx, sid, false)
|
||||
|
||||
// Multi-char -ow: should de-oper and remove wallops.
|
||||
_ = env.db.SetSessionWallops(ctx, sid, true)
|
||||
|
||||
result, err = env.svc.ApplyUserMode(ctx, sid, "-ow")
|
||||
if err != nil {
|
||||
t.Fatalf("apply -ow: %v", err)
|
||||
}
|
||||
|
||||
if result != "+" {
|
||||
t.Errorf("expected +, got %s", result)
|
||||
}
|
||||
|
||||
// +wo should fail because +o is not allowed.
|
||||
_, err = env.svc.ApplyUserMode(ctx, sid, "+wo")
|
||||
|
||||
var ircErr *service.IRCError
|
||||
if !errors.As(err, &ircErr) {
|
||||
t.Fatalf("expected IRCError, got %v", err)
|
||||
}
|
||||
|
||||
if ircErr.Code != irc.ErrUmodeUnknownFlag {
|
||||
t.Errorf(
|
||||
"expected ErrUmodeUnknownFlag, got %d",
|
||||
ircErr.Code,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyUserModeInvalidInput(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid := createSession(ctx, t, env.db, "alice")
|
||||
|
||||
// Too short.
|
||||
_, err := env.svc.ApplyUserMode(ctx, sid, "+")
|
||||
|
||||
var ircErr *service.IRCError
|
||||
if !errors.As(err, &ircErr) {
|
||||
t.Fatalf("expected IRCError for short input, got %v", err)
|
||||
}
|
||||
|
||||
// Unknown flag.
|
||||
_, err = env.svc.ApplyUserMode(ctx, sid, "+x")
|
||||
if !errors.As(err, &ircErr) {
|
||||
t.Fatalf("expected IRCError for unknown flag, got %v", err)
|
||||
}
|
||||
|
||||
if ircErr.Code != irc.ErrUmodeUnknownFlag {
|
||||
t.Errorf(
|
||||
"expected ErrUmodeUnknownFlag, got %d",
|
||||
ircErr.Code,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyUserModeDeoper(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid := createSession(ctx, t, env.db, "alice")
|
||||
|
||||
// Make oper via DB directly.
|
||||
_ = env.db.SetSessionOper(ctx, sid, true)
|
||||
|
||||
// -o should work.
|
||||
result, err := env.svc.ApplyUserMode(ctx, sid, "-o")
|
||||
if err != nil {
|
||||
t.Fatalf("apply -o: %v", err)
|
||||
}
|
||||
|
||||
if result != "+" {
|
||||
t.Errorf("expected +, got %s", result)
|
||||
}
|
||||
|
||||
// +o should fail.
|
||||
_, err = env.svc.ApplyUserMode(ctx, sid, "+o")
|
||||
|
||||
var ircErr *service.IRCError
|
||||
if !errors.As(err, &ircErr) {
|
||||
t.Fatalf("expected IRCError for +o, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user