chore(lint): errorlint + exhaustive + switch-exhaustiveness-check, and permissions.deny for generated files (#1462)

* chore(claude): deny hand-edits to generated files via permissions.deny

CLAUDE.md already says the sqlc, protocol and tauri-typegen outputs are
never hand-edited; this turns the sentence into a permission rule so the
Edit/Write tools refuse those paths outright.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwaz4CHGAz85Rpypjvto5a

* chore(lint): switch-exhaustiveness-check on the client, default branch counts as exhaustive

A switch over a string union that misses a member is a silent drop, not a
type error. Every existing default-less switch already covers its union, so
this adds no exceptions; the four switches with a default keep it as the
deliberate catch-all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwaz4CHGAz85Rpypjvto5a

* chore(lint): enable errorlint and exhaustive in golangci and fix the 110 hits

errorlint: 68 fmt.Errorf sites wrapped the inner error with %v, which hid it
from errors.Is/As upstream — now %w; 6 == / != comparisons on sentinel
errors become errors.Is (the recover() branch in the router asserts the
recovered value is an error first); 36 ClientError type assertions become
errors.As, so a wrapped ClientError still reaches the client with its code.
Three test assertions the autofixer inverted (!ok || code mismatch) are
restored by hand.

exhaustive (default-signifies-exhaustive): one hit, the hub simulation's
FaultStatus switch — FaultOK moves from an if-guard into the switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwaz4CHGAz85Rpypjvto5a

* chore(claude): path-scoped rules for the three generated-code workflows

.claude/rules/{db-change,protocol-change,gendocs}.md load only when Claude
reads a matching source-of-truth file, so the db-change / protocol-change
skills and the gendocs regeneration step surface at the moment they apply
instead of relying on the CLAUDE.md table being remembered. .gitignore
whitelists .claude/rules/ next to skills/, workflows/ and settings.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwaz4CHGAz85Rpypjvto5a

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-08-30 20:22:55 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 526011141f
commit ead64cdc20
38 changed files with 248 additions and 112 deletions
+20
View File
@@ -0,0 +1,20 @@
---
paths:
- "Server/db/queries/**"
- "Server/migrations/**"
- "Server/sqlc.yaml"
---
# Schema and query edits
These files are the sqlc source of truth. Invoke the `db-change` skill before
changing anything here.
`Server/db/dbgen/` is generated from them and is denied to Edit/Write in
`.claude/settings.json`. After a change, regenerate and stage the result:
```
cd Server && sqlc generate
```
The pre-commit hook and CI (`make sqlc-verify`) both fail on drift.
+19
View File
@@ -0,0 +1,19 @@
---
paths:
- "Server/api/router.go"
- "Server/config/config.go"
- "Server/migrations/**"
---
# Generated documentation blocks
The `gendocs:*` blocks in `docs/api.md`, `docs/schema.md` and
`docs/server-configuration.md` are generated from these files. Never edit
inside a `gendocs:*` block by hand. After changing routes, config fields or
migrations, regenerate and stage the docs:
```
cd Server && go run -tags otel,wazero ./cmd/gendocs
```
CI (`make docs-verify`) fails on drift.
+20
View File
@@ -0,0 +1,20 @@
---
paths:
- "protocol/schema.json"
- "Server/cmd/genprotocol/**"
---
# Protocol message types
`protocol/schema.json` is the source of truth for the WebSocket message types.
Invoke the `protocol-change` skill before changing it.
`Server/ws/message_types.go` and `Client/src/lib/protocolTypes.ts` are generated
from it and are denied to Edit/Write in `.claude/settings.json`. After a change,
regenerate and stage both files:
```
cd Server && go run ./cmd/genprotocol
```
The pre-commit hook and CI (`make protocol-verify`) both fail on drift.
+14 -1
View File
@@ -1 +1,14 @@
{}
{
"permissions": {
"deny": [
"Edit(Server/db/dbgen/**)",
"Write(Server/db/dbgen/**)",
"Edit(Server/ws/message_types.go)",
"Write(Server/ws/message_types.go)",
"Edit(Client/src/lib/protocolTypes.ts)",
"Write(Client/src/lib/protocolTypes.ts)",
"Edit(Client/src/generated/**)",
"Write(Client/src/generated/**)"
]
}
}
+1
View File
@@ -9,6 +9,7 @@ Server/.env
.claude/*
!.claude/skills/
!.claude/workflows/
!.claude/rules/
!.claude/settings.json
CLAUDE.local.md
.mcp.json
+5
View File
@@ -15,6 +15,11 @@ export default tseslint.config(
rules: {
// --- Key rules from T-191 ---
"@typescript-eslint/no-floating-promises": "error",
// A switch over a union that misses a member is a silent drop, not a type error.
"@typescript-eslint/switch-exhaustiveness-check": [
"error",
{ considerDefaultExhaustiveForUnions: true },
],
"@typescript-eslint/no-unused-vars": [
"error",
{
+8
View File
@@ -25,7 +25,15 @@ linters:
- nestif # deeply nested if-blocks
- dupl # verbatim duplicated blocks
# Silent-drop and sentinel-error classes (added 2026-08-30).
- exhaustive # a switch over an enum-like type with no default that misses a member
- errorlint # errors.Is/As instead of == and type assertions on wrapped errors; %w in Errorf
settings:
exhaustive:
# An explicit default branch is a deliberate catch-all; only switches without
# one, which fall through in silence, must list every member.
default-signifies-exhaustive: true
staticcheck:
checks:
- "all"
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
@@ -653,7 +654,7 @@ func recoverer(next http.Handler) http.Handler {
defer func() {
if rec := recover(); rec != nil {
// Preserve chi's behaviour of not swallowing the abort sentinel.
if rec == http.ErrAbortHandler {
if err, ok := rec.(error); ok && errors.Is(err, http.ErrAbortHandler) {
panic(rec)
}
attrs := []any{
+2 -1
View File
@@ -2,6 +2,7 @@ package db_test
import (
"context"
"errors"
"testing"
"time"
@@ -52,7 +53,7 @@ func TestVoice_JoinVoiceChannelIfCapacity_AtLimit(t *testing.T) {
if err == nil {
t.Fatal("expected ErrChannelFull, got nil")
}
if err != db.ErrChannelFull {
if !errors.Is(err, db.ErrChannelFull) {
t.Errorf("error = %v, want ErrChannelFull", err)
}
}
+3 -3
View File
@@ -188,20 +188,20 @@ func GuardedDialContext() func(ctx context.Context, network, addr string) (net.C
// IP literal: validate and dial as-is (no resolution happens).
if ip := net.ParseIP(h); ip != nil {
if err := ipAllowed(ip); err != nil {
return nil, fmt.Errorf("%w: %v", ErrHTTPHostDenied, err)
return nil, fmt.Errorf("%w: %w", ErrHTTPHostDenied, err)
}
return dialContext(ctx, network, addr)
}
ips, lookupErr := lookupIPAddr(ctx, h)
if lookupErr != nil {
return nil, fmt.Errorf("%w: dns lookup failed: %v", ErrHTTPHostDenied, lookupErr)
return nil, fmt.Errorf("%w: dns lookup failed: %w", ErrHTTPHostDenied, lookupErr)
}
if len(ips) == 0 {
return nil, fmt.Errorf("%w: no addresses for %s", ErrHTTPHostDenied, h)
}
for _, resolved := range ips {
if err := ipAllowed(resolved.IP); err != nil {
return nil, fmt.Errorf("%w: %v", ErrHTTPHostDenied, err)
return nil, fmt.Errorf("%w: %w", ErrHTTPHostDenied, err)
}
}
var dialErr error
+1 -1
View File
@@ -409,7 +409,7 @@ func installZipWriteEntry(f *zip.File, destAbs string, remaining int64) (int64,
n, copyErr := io.CopyN(out, rc, remaining+1)
_ = rc.Close()
_ = out.Close()
if copyErr != nil && copyErr != io.EOF {
if copyErr != nil && !errors.Is(copyErr, io.EOF) {
return 0, copyErr
}
if n > remaining {
+3 -3
View File
@@ -46,7 +46,7 @@ func (s *BlockService) BlockUser(ctx context.Context, blockerID, targetID int64)
}
if err := s.st.BlockUser(ctx, blockerID, targetID); err != nil {
return fmt.Errorf("%w: failed to block user: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to block user: %w", ErrInternal, err)
}
slog.Info("user blocked", "blocker_id", blockerID, "target_id", targetID)
@@ -59,7 +59,7 @@ func (s *BlockService) UnblockUser(ctx context.Context, blockerID, targetID int6
return fmt.Errorf("%w: user_id must be positive", ErrBadRequest)
}
if err := s.st.UnblockUser(ctx, blockerID, targetID); err != nil {
return fmt.Errorf("%w: failed to unblock user: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to unblock user: %w", ErrInternal, err)
}
slog.Info("user unblocked", "blocker_id", blockerID, "target_id", targetID)
return nil
@@ -69,7 +69,7 @@ func (s *BlockService) UnblockUser(ctx context.Context, blockerID, targetID int6
func (s *BlockService) ListBlocked(ctx context.Context, blockerID int64) ([]int64, error) {
ids, err := s.st.ListBlockedUsers(ctx, blockerID)
if err != nil {
return nil, fmt.Errorf("%w: failed to list blocked users: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list blocked users: %w", ErrInternal, err)
}
if ids == nil {
ids = []int64{}
+1 -1
View File
@@ -253,7 +253,7 @@ func (s *ChannelService) HandleChannelFocus(ctx context.Context, userID, channel
return nil, fmt.Errorf("%w: access denied", ErrForbidden)
}
if err := permissions.CanAdmitSession(sub); err != nil {
return nil, fmt.Errorf("%w: %v", ErrForbidden, err)
return nil, fmt.Errorf("%w: %w", ErrForbidden, err)
}
// Mark channel as read. latestID == 0 (no undeleted messages) still
+14 -14
View File
@@ -129,7 +129,7 @@ func (s *DMService) CreateDM(ctx context.Context, userID, recipientID int64) (*C
blocked, err := s.st.IsEitherBlocked(ctx, userID, recipientID)
if err != nil {
return nil, fmt.Errorf("%w: failed to check block status: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to check block status: %w", ErrInternal, err)
}
if blocked {
return nil, fmt.Errorf("%w: cannot create DM — user is blocked", ErrForbidden)
@@ -152,7 +152,7 @@ func (s *DMService) CreateDM(ctx context.Context, userID, recipientID int64) (*C
func (s *DMService) ListDMs(ctx context.Context, userID int64) ([]db.DMChannelInfo, error) {
dms, err := s.st.GetUserDMChannels(ctx, userID)
if err != nil {
return nil, fmt.Errorf("%w: failed to list DMs: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list DMs: %w", ErrInternal, err)
}
// GetUserDMChannels only applies db.StatusForViewer (invisible ->
// offline); apply the "no live connection" half too, see
@@ -198,12 +198,12 @@ func (s *DMService) CloseDM(ctx context.Context, userID, channelID int64) (*Clos
isGroup, err := s.st.IsGroupDM(ctx, channelID)
if err != nil {
return nil, fmt.Errorf("%w: failed to read DM kind: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to read DM kind: %w", ErrInternal, err)
}
if !isGroup {
if err := s.st.CloseDM(ctx, userID, channelID); err != nil {
return nil, fmt.Errorf("%w: failed to close DM: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to close DM: %w", ErrInternal, err)
}
slog.Debug("DM closed", "user_id", userID, "channel_id", channelID)
return &CloseDMResult{}, nil
@@ -214,7 +214,7 @@ func (s *DMService) CloseDM(ctx context.Context, userID, channelID int64) (*Clos
// never in it" if the delete half-succeeded.
remaining, err := s.st.GetDMParticipantIDs(ctx, channelID)
if err != nil {
return nil, fmt.Errorf("%w: failed to read DM participants: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to read DM participants: %w", ErrInternal, err)
}
survivors := make([]int64, 0, len(remaining))
for _, pid := range remaining {
@@ -225,7 +225,7 @@ func (s *DMService) CloseDM(ctx context.Context, userID, channelID int64) (*Clos
deleted, err := s.st.LeaveGroupDM(ctx, userID, channelID)
if err != nil {
return nil, fmt.Errorf("%w: failed to leave group DM: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to leave group DM: %w", ErrInternal, err)
}
slog.Debug("group DM left", "user_id", userID, "channel_id", channelID, "deleted", deleted)
@@ -330,7 +330,7 @@ func (s *DMService) CreateGroupDM(ctx context.Context, userID int64, recipientID
for j := i + 1; j < len(participantIDs); j++ {
blocked, err := s.st.IsEitherBlocked(ctx, participantIDs[i], participantIDs[j])
if err != nil {
return nil, fmt.Errorf("%w: failed to check block status: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to check block status: %w", ErrInternal, err)
}
if blocked {
return nil, fmt.Errorf("%w: cannot add a blocked user to a group DM", ErrForbidden)
@@ -389,7 +389,7 @@ func (s *DMService) RenameGroupDM(ctx context.Context, userID, channelID int64,
isGroup, err := s.st.IsGroupDM(ctx, channelID)
if err != nil {
return nil, fmt.Errorf("%w: failed to read DM kind: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to read DM kind: %w", ErrInternal, err)
}
if !isGroup {
return nil, fmt.Errorf("%w: only group DMs can be named", ErrBadRequest)
@@ -403,7 +403,7 @@ func (s *DMService) RenameGroupDM(ctx context.Context, userID, channelID int64,
}
if err := s.st.SetDMChannelName(ctx, channelID, cleanName); err != nil {
return nil, fmt.Errorf("%w: failed to rename group DM: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to rename group DM: %w", ErrInternal, err)
}
ch, err := s.st.GetChannel(ctx, channelID)
@@ -427,7 +427,7 @@ func (s *DMService) DMSummaryFor(ctx context.Context, viewerID, channelID int64)
}
participants, err := s.st.GetDMParticipants(ctx, channelID, viewerID)
if err != nil {
return db.DMChannelInfo{}, fmt.Errorf("%w: failed to read DM participants: %v", ErrInternal, err)
return db.DMChannelInfo{}, fmt.Errorf("%w: failed to read DM participants: %w", ErrInternal, err)
}
ch, err := s.st.GetChannel(ctx, channelID)
if err != nil || ch == nil {
@@ -435,7 +435,7 @@ func (s *DMService) DMSummaryFor(ctx context.Context, viewerID, channelID int64)
}
isGroup, err := s.st.IsGroupDM(ctx, channelID)
if err != nil {
return db.DMChannelInfo{}, fmt.Errorf("%w: failed to read DM kind: %v", ErrInternal, err)
return db.DMChannelInfo{}, fmt.Errorf("%w: failed to read DM kind: %w", ErrInternal, err)
}
// See presentableDMChannelInfo: this is the single place broadcastDMOpen
// (group create/rename/leave refresh) and PATCH /dms/{id}'s response
@@ -450,7 +450,7 @@ func (s *DMService) DMSummaryFor(ctx context.Context, viewerID, channelID int64)
func (s *DMService) SharedOneToOneDM(ctx context.Context, userA, userB int64) (int64, bool, error) {
id, ok, err := s.st.FindDMChannelIDBetween(ctx, userA, userB)
if err != nil {
return 0, false, fmt.Errorf("%w: failed to look up shared DM: %v", ErrInternal, err)
return 0, false, fmt.Errorf("%w: failed to look up shared DM: %w", ErrInternal, err)
}
return id, ok, nil
}
@@ -469,7 +469,7 @@ func (s *DMService) RingTargets(ctx context.Context, userID, channelID int64) ([
}
ok, err := s.st.IsDMParticipant(ctx, userID, channelID)
if err != nil {
return nil, fmt.Errorf("%w: failed to check DM participation: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to check DM participation: %w", ErrInternal, err)
}
if !ok {
return nil, fmt.Errorf("%w: not a participant in this DM", ErrForbidden)
@@ -484,7 +484,7 @@ func (s *DMService) RingTargets(ctx context.Context, userID, channelID int64) ([
ids, err := s.st.GetDMParticipantIDs(ctx, channelID)
if err != nil {
return nil, fmt.Errorf("%w: failed to read DM participants: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to read DM participants: %w", ErrInternal, err)
}
targets := make([]int64, 0, len(ids))
for _, pid := range ids {
+7 -7
View File
@@ -102,7 +102,7 @@ func (s *EmojiService) RequireManage(ctx context.Context, actorID int64) error {
func (s *EmojiService) List(ctx context.Context) ([]*db.Emoji, error) {
list, err := s.st.ListEmoji(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to list emoji: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list emoji: %w", ErrInternal, err)
}
return list, nil
}
@@ -125,7 +125,7 @@ func (s *EmojiService) Create(ctx context.Context, actorID int64, rawShortcode,
existing, err := s.st.GetEmojiByShortcode(ctx, shortcode)
if err != nil {
return nil, fmt.Errorf("%w: failed to check shortcode: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to check shortcode: %w", ErrInternal, err)
}
if existing != nil {
return nil, fmt.Errorf("%w: an emoji named :%s: already exists", ErrConflict, shortcode)
@@ -133,7 +133,7 @@ func (s *EmojiService) Create(ctx context.Context, actorID int64, rawShortcode,
current, err := s.st.ListEmoji(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to count emoji: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to count emoji: %w", ErrInternal, err)
}
if len(current) >= MaxEmojiCount {
return nil, fmt.Errorf("%w: this server already has the maximum of %d emoji", ErrBadRequest, MaxEmojiCount)
@@ -147,7 +147,7 @@ func (s *EmojiService) Create(ctx context.Context, actorID int64, rawShortcode,
// server fault.
return nil, fmt.Errorf("%w: an emoji named :%s: already exists", ErrConflict, shortcode)
}
return nil, fmt.Errorf("%w: failed to create emoji: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to create emoji: %w", ErrInternal, err)
}
db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "emoji_create", "emoji", created.ID,
@@ -165,14 +165,14 @@ func (s *EmojiService) Delete(ctx context.Context, actorID, emojiID int64) (*db.
}
existing, err := s.st.GetEmoji(ctx, emojiID)
if err != nil {
return nil, fmt.Errorf("%w: failed to load emoji: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to load emoji: %w", ErrInternal, err)
}
if existing == nil {
return nil, fmt.Errorf("%w: emoji not found", ErrNotFound)
}
deleted, err := s.st.DeleteEmoji(ctx, emojiID)
if err != nil {
return nil, fmt.Errorf("%w: failed to delete emoji: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to delete emoji: %w", ErrInternal, err)
}
if !deleted {
// Lost a race with another delete -- report it as the 404 it now is.
@@ -189,7 +189,7 @@ func (s *EmojiService) Delete(ctx context.Context, actorID, emojiID int64) (*db.
func (s *EmojiService) Get(ctx context.Context, emojiID int64) (*db.Emoji, error) {
e, err := s.st.GetEmoji(ctx, emojiID)
if err != nil {
return nil, fmt.Errorf("%w: failed to load emoji: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to load emoji: %w", ErrInternal, err)
}
if e == nil {
return nil, fmt.Errorf("%w: emoji not found", ErrNotFound)
+4 -4
View File
@@ -50,7 +50,7 @@ func (s *InviteService) CreateInvite(ctx context.Context, createdBy int64, maxUs
code, err := s.st.CreateInvite(ctx, createdBy, maxUses, expiresAt)
if err != nil {
return nil, fmt.Errorf("%w: failed to create invite: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to create invite: %w", ErrInternal, err)
}
// The invite is committed from here on: a request canceled during the
@@ -58,7 +58,7 @@ func (s *InviteService) CreateInvite(ctx context.Context, createdBy int64, maxUs
tailCtx := context.WithoutCancel(ctx)
invite, err := s.st.GetInvite(tailCtx, code)
if err != nil || invite == nil {
return nil, fmt.Errorf("%w: failed to retrieve invite: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to retrieve invite: %w", ErrInternal, err)
}
// S-02: the row names the invite by id, never by code; the code is the
// credential.
@@ -71,7 +71,7 @@ func (s *InviteService) CreateInvite(ctx context.Context, createdBy int64, maxUs
func (s *InviteService) ListInvites(ctx context.Context) ([]*db.Invite, error) {
invites, err := s.st.ListInvites(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to list invites: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list invites: %w", ErrInternal, err)
}
return invites, nil
}
@@ -83,7 +83,7 @@ func (s *InviteService) RevokeInvite(ctx context.Context, actorID int64, code st
return fmt.Errorf("%w: invite not found", ErrNotFound)
}
if err := s.st.RevokeInvite(ctx, code); err != nil {
return fmt.Errorf("%w: failed to revoke invite: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to revoke invite: %w", ErrInternal, err)
}
db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "invite_revoke", "invite", invite.ID, "")
return nil
+7 -7
View File
@@ -13,12 +13,12 @@ import (
func (s *MessageService) GetAccessibleChannelIDs(ctx context.Context, userID int64) ([]int64, error) {
channels, err := s.st.ListChannels(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to list channels: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list channels: %w", ErrInternal, err)
}
role, err := s.perms.GetRoleForUser(ctx, userID)
if err != nil || role == nil {
return nil, fmt.Errorf("%w: failed to get role: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to get role: %w", ErrInternal, err)
}
var overrides map[int64]db.ChannelOverride
@@ -26,7 +26,7 @@ func (s *MessageService) GetAccessibleChannelIDs(ctx context.Context, userID int
var overrideErr error
overrides, overrideErr = s.st.GetChannelOverridesFor(ctx, role.ID, userID)
if overrideErr != nil {
return nil, fmt.Errorf("%w: failed to fetch channel overrides: %v", ErrInternal, overrideErr)
return nil, fmt.Errorf("%w: failed to fetch channel overrides: %w", ErrInternal, overrideErr)
}
}
@@ -50,7 +50,7 @@ func (s *MessageService) GetAccessibleChannelIDs(ctx context.Context, userID int
// computeAllowedChannels in ws/serve.go.
dmIDs, err := s.st.GetUserDMChannelIDs(ctx, userID)
if err != nil {
return nil, fmt.Errorf("%w: failed to fetch DM channels: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to fetch DM channels: %w", ErrInternal, err)
}
ids = append(ids, dmIDs...)
@@ -101,7 +101,7 @@ func channelSubject(ctx context.Context, st Store, perms *PermissionService, use
}
ok, dmErr := st.IsDMParticipant(ctx, userID, ch.ID)
if dmErr != nil {
return sub, fmt.Errorf("%w: failed to check DM participation: %v", ErrInternal, dmErr)
return sub, fmt.Errorf("%w: failed to check DM participation: %w", ErrInternal, dmErr)
}
sub.DMParticipant = ok
if ok && withBlock {
@@ -125,7 +125,7 @@ func denial(err error) error {
case errors.Is(err, permissions.ErrBlocked):
return fmt.Errorf("%w: user is blocked", ErrBlocked)
default:
return fmt.Errorf("%w: %v", ErrForbidden, err)
return fmt.Errorf("%w: %w", ErrForbidden, err)
}
}
@@ -199,7 +199,7 @@ func requireDMNotBlocked(ctx context.Context, st Store, userID, channelID int64)
}
blocked, blkErr := st.IsEitherBlocked(ctx, userID, recipient.ID)
if blkErr != nil {
return fmt.Errorf("%w: failed to check block status: %v", ErrInternal, blkErr)
return fmt.Errorf("%w: failed to check block status: %w", ErrInternal, blkErr)
}
if blocked {
return fmt.Errorf("%w: user is blocked", ErrBlocked)
+4 -4
View File
@@ -92,7 +92,7 @@ func (s *MessageService) SearchMessages(ctx context.Context, userID int64, query
}
results, err := s.st.SearchMessages(ctx, query, channelID, limit)
if err != nil {
return nil, fmt.Errorf("%w: search failed: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: search failed: %w", ErrInternal, err)
}
return results, nil
}
@@ -108,7 +108,7 @@ func (s *MessageService) SearchMessages(ctx context.Context, userID int64, query
results, err := s.st.SearchMessagesInChannels(ctx, query, accessibleIDs, limit)
if err != nil {
return nil, fmt.Errorf("%w: search failed: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: search failed: %w", ErrInternal, err)
}
return results, nil
}
@@ -191,7 +191,7 @@ func (s *MessageService) GetPinnedMessages(ctx context.Context, userID, channelI
}
msgs, err := s.st.GetPinnedMessages(ctx, channelID, userID)
if err != nil {
return nil, fmt.Errorf("%w: failed to fetch pinned messages: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to fetch pinned messages: %w", ErrInternal, err)
}
return msgs, nil
}
@@ -242,7 +242,7 @@ func (s *MessageService) SetMessagePinned(ctx context.Context, userID, channelID
if errors.Is(err, db.ErrNotFound) {
return fmt.Errorf("%w: message not found in this channel", ErrNotFound)
}
return fmt.Errorf("%w: %v", ErrInternal, err)
return fmt.Errorf("%w: %w", ErrInternal, err)
}
return nil
}
+4 -4
View File
@@ -120,7 +120,7 @@ func (s *ModerationService) BanUser(ctx context.Context, actorID, targetID int64
}
if err := s.st.BanUser(ctx, targetID, reason, expires); err != nil {
return fmt.Errorf("%w: failed to ban user: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to ban user: %w", ErrInternal, err)
}
// Audit rows must survive a request canceled after the ban committed.
@@ -194,7 +194,7 @@ func (s *ModerationService) ChangeUserRole(ctx context.Context, actorID, targetI
}
if err := s.st.UpdateUserRole(ctx, targetID, newRoleID); err != nil {
return nil, fmt.Errorf("%w: failed to update role: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to update role: %w", ErrInternal, err)
}
// Drop the target's cached role immediately: without this a demotion keeps
// granting the old bits (and the old rank) for up to permCacheTTL.
@@ -233,7 +233,7 @@ func (s *ModerationService) ForceLogout(ctx context.Context, actorID, targetID i
}
if err := s.st.ForceLogoutUser(ctx, targetID); err != nil {
return fmt.Errorf("%w: failed to log out user: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to log out user: %w", ErrInternal, err)
}
// Audit rows must survive a request canceled after the sessions were cut.
@@ -263,7 +263,7 @@ func (s *ModerationService) UnbanUser(ctx context.Context, actorID, targetID int
}
if err := s.st.UnbanUser(ctx, targetID); err != nil {
return fmt.Errorf("%w: failed to unban user: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to unban user: %w", ErrInternal, err)
}
// Audit rows must survive a request canceled after the unban committed.
+14 -14
View File
@@ -124,7 +124,7 @@ func (s *RoleService) validateName(ctx context.Context, raw string, excludeID in
}
existing, err := s.st.GetRoleByName(ctx, name)
if err != nil {
return "", fmt.Errorf("%w: failed to check role name: %v", ErrInternal, err)
return "", fmt.Errorf("%w: failed to check role name: %w", ErrInternal, err)
}
if existing != nil && existing.ID != excludeID {
return "", fmt.Errorf("%w: a role named %q already exists", ErrBadRequest, existing.Name)
@@ -169,11 +169,11 @@ func (s *RoleService) ListRoles(ctx context.Context, actorID int64) ([]RoleWithM
}
roles, err := s.st.ListRoles(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list roles: %w", ErrInternal, err)
}
counts, err := s.st.CountRoleMembers(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to count role members: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to count role members: %w", ErrInternal, err)
}
out := make([]RoleWithMembers, 0, len(roles))
for _, r := range roles {
@@ -216,7 +216,7 @@ func (s *RoleService) CreateRole(ctx context.Context, actorID int64, in RoleInpu
existing, err := s.st.ListRoles(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list roles: %w", ErrInternal, err)
}
if len(existing) >= maxRoles {
return nil, fmt.Errorf("%w: server already has the maximum of %d roles", ErrBadRequest, maxRoles)
@@ -259,7 +259,7 @@ func (s *RoleService) CreateRole(ctx context.Context, actorID int64, in RoleInpu
role, err := s.st.CreateRole(ctx, name, color, perms, position)
if err != nil {
return nil, fmt.Errorf("%w: failed to create role: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to create role: %w", ErrInternal, err)
}
db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "role_create", "role", role.ID,
@@ -281,7 +281,7 @@ func (s *RoleService) UpdateRole(ctx context.Context, actorID, roleID int64, in
}
role, err := s.st.GetRoleByID(ctx, roleID)
if err != nil {
return nil, false, fmt.Errorf("%w: failed to fetch role: %v", ErrInternal, err)
return nil, false, fmt.Errorf("%w: failed to fetch role: %w", ErrInternal, err)
}
if role == nil {
return nil, false, fmt.Errorf("%w: role not found", ErrNotFound)
@@ -321,7 +321,7 @@ func (s *RoleService) UpdateRole(ctx context.Context, actorID, roleID int64, in
if position != role.Position {
existing, err := s.st.ListRoles(ctx)
if err != nil {
return nil, false, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
return nil, false, fmt.Errorf("%w: failed to list roles: %w", ErrInternal, err)
}
for _, rl := range existing {
if rl.ID != role.ID && rl.Position == position {
@@ -332,7 +332,7 @@ func (s *RoleService) UpdateRole(ctx context.Context, actorID, roleID int64, in
}
if err := s.st.UpdateRole(ctx, role.ID, name, color, perms, position); err != nil {
return nil, false, fmt.Errorf("%w: failed to update role: %v", ErrInternal, err)
return nil, false, fmt.Errorf("%w: failed to update role: %w", ErrInternal, err)
}
db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "role_update", "role", role.ID,
@@ -361,7 +361,7 @@ func (s *RoleService) DeleteRole(ctx context.Context, actorID, roleID int64) (de
}
role, err := s.st.GetRoleByID(ctx, roleID)
if err != nil {
return nil, nil, nil, fmt.Errorf("%w: failed to fetch role: %v", ErrInternal, err)
return nil, nil, nil, fmt.Errorf("%w: failed to fetch role: %w", ErrInternal, err)
}
if role == nil {
return nil, nil, nil, fmt.Errorf("%w: role not found", ErrNotFound)
@@ -382,7 +382,7 @@ func (s *RoleService) DeleteRole(ctx context.Context, actorID, roleID int64) (de
fallback, err = s.st.GetDefaultRole(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("%w: failed to resolve the default role: %v", ErrInternal, err)
return nil, nil, nil, fmt.Errorf("%w: failed to resolve the default role: %w", ErrInternal, err)
}
if fallback == nil {
// Fail closed: without a fallback the members would be orphaned on a
@@ -392,7 +392,7 @@ func (s *RoleService) DeleteRole(ctx context.Context, actorID, roleID int64) (de
movedUserIDs, err = s.st.DeleteRoleReassigning(ctx, role.ID, fallback.ID)
if err != nil {
return nil, nil, nil, fmt.Errorf("%w: failed to delete role: %v", ErrInternal, err)
return nil, nil, nil, fmt.Errorf("%w: failed to delete role: %w", ErrInternal, err)
}
// The members' cached masks are the deleted role's until this drops them.
if s.perms != nil {
@@ -426,7 +426,7 @@ func (s *RoleService) ReorderRoles(ctx context.Context, actorID int64, orderedID
}
roles, err := s.st.ListRoles(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list roles: %w", ErrInternal, err)
}
manageable := make(map[int64]*db.Role, len(roles))
@@ -459,7 +459,7 @@ func (s *RoleService) ReorderRoles(ctx context.Context, actorID int64, orderedID
positions[id] = len(orderedIDs) - i
}
if err := s.st.SetRolePositions(ctx, positions); err != nil {
return nil, fmt.Errorf("%w: failed to reorder roles: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to reorder roles: %w", ErrInternal, err)
}
db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "role_reorder", "role", 0,
@@ -468,7 +468,7 @@ func (s *RoleService) ReorderRoles(ctx context.Context, actorID int64, orderedID
updated, err := s.st.ListRoles(ctx)
if err != nil {
return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list roles: %w", ErrInternal, err)
}
return updated, nil
}
+6 -6
View File
@@ -225,7 +225,7 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, patch Pro
if db.IsUniqueConstraintError(err) {
return nil, fmt.Errorf("%w: username is already taken", ErrConflict)
}
return nil, fmt.Errorf("%w: failed to update profile: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to update profile: %w", ErrInternal, err)
}
// This re-read, like the audit write below, must survive a request ctx
// canceled after the write above committed — otherwise a client that
@@ -273,7 +273,7 @@ func (s *UserService) SetCustomStatus(ctx context.Context, userID int64, text st
return err
}
if err := s.st.UpdateUserCustomStatus(ctx, userID, nullable(cleaned)); err != nil {
return fmt.Errorf("%w: failed to update custom status: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to update custom status: %w", ErrInternal, err)
}
return nil
}
@@ -283,7 +283,7 @@ func (s *UserService) SetCustomStatus(ctx context.Context, userID int64, text st
// user signed out states something about them that is no longer true.
func (s *UserService) ClearCustomStatus(ctx context.Context, userID int64) error {
if err := s.st.UpdateUserCustomStatus(ctx, userID, nil); err != nil {
return fmt.Errorf("%w: failed to clear custom status: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to clear custom status: %w", ErrInternal, err)
}
return nil
}
@@ -324,7 +324,7 @@ type ChangePasswordResult struct {
// ChangePassword updates the user's password and revokes other sessions.
func (s *UserService) ChangePassword(ctx context.Context, userID int64, newPasswordHash string, keepSessionID int64) (ChangePasswordResult, error) {
if err := s.st.UpdateUserPassword(ctx, userID, newPasswordHash); err != nil {
return ChangePasswordResult{}, fmt.Errorf("%w: failed to update password: %v", ErrInternal, err)
return ChangePasswordResult{}, fmt.Errorf("%w: failed to update password: %w", ErrInternal, err)
}
// The password is committed from here on: every path below reports
@@ -356,7 +356,7 @@ func (s *UserService) ChangePassword(ctx context.Context, userID int64, newPassw
func (s *UserService) ListSessions(ctx context.Context, userID int64) ([]db.Session, error) {
sessions, err := s.st.ListUserSessions(ctx, userID)
if err != nil {
return nil, fmt.Errorf("%w: failed to list sessions: %v", ErrInternal, err)
return nil, fmt.Errorf("%w: failed to list sessions: %w", ErrInternal, err)
}
return sessions, nil
}
@@ -367,7 +367,7 @@ func (s *UserService) RevokeSession(ctx context.Context, userID, sessionID int64
if errors.Is(err, db.ErrNotFound) {
return fmt.Errorf("%w: session not found", ErrNotFound)
}
return fmt.Errorf("%w: failed to revoke session: %v", ErrInternal, err)
return fmt.Errorf("%w: failed to revoke session: %w", ErrInternal, err)
}
// Audit rows must survive a request canceled after the delete committed.
db.WriteAudit(context.WithoutCancel(ctx), s.st, userID, "session_revoke", "session", sessionID, "session revoked")
+1 -1
View File
@@ -124,7 +124,7 @@ func (s *Storage) Save(uuid string, r io.Reader) (int64, error) {
// Read the first 8 bytes to check magic bytes without consuming the stream.
var header [8]byte
n, err := io.ReadFull(r, header[:])
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
return 0, fmt.Errorf("reading file header: %w", err)
}
headerSlice := header[:n]
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
@@ -193,7 +194,7 @@ func extractChatserverFromTarGz(r io.Reader, destPath string) (string, error) {
tr := tar.NewReader(gr)
for {
hdr, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
return "", fmt.Errorf("archive contains no file named chatserver")
}
if err != nil {
+3 -1
View File
@@ -2,6 +2,7 @@ package ws
import (
"bytes"
"errors"
"testing"
)
@@ -35,7 +36,8 @@ func TestResultWithError(t *testing.T) {
if r.Error == nil {
t.Fatal("expected non-nil Error")
}
ce, ok := r.Error.(ClientError)
var ce ClientError
ok := errors.As(r.Error, &ce)
if !ok {
t.Fatal("expected ClientError type")
}
+3 -1
View File
@@ -2,6 +2,7 @@ package ws
import (
"context"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/auth"
@@ -122,7 +123,8 @@ func TestChannelFocusV2_NoPermission_ReturnsForbidden(t *testing.T) {
if result.Error == nil {
t.Fatal("expected FORBIDDEN error for denied permission")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
+9 -4
View File
@@ -7,6 +7,7 @@ package ws
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/auth"
@@ -77,7 +78,8 @@ func TestHandleVoiceLeaveV2_RateLimited(t *testing.T) {
for range voiceLeaveRateLimit + 1 {
res := handleVoiceLeaveV2(context.Background(), cmd, info, deps)
if res.Error != nil {
ce, ok := res.Error.(ClientError)
var ce ClientError
ok := errors.As(res.Error, &ce)
if !ok || ce.Code != ErrCodeRateLimited {
t.Fatalf("expected rate-limit ClientError, got %v", res.Error)
}
@@ -108,7 +110,8 @@ func TestHandleVoiceLeaveV2_RateLimited_StillSignalsLeave(t *testing.T) {
for range voiceLeaveRateLimit + 1 {
res := handleVoiceLeaveV2(context.Background(), cmd, info, deps)
if res.Error != nil {
ce, ok := res.Error.(ClientError)
var ce ClientError
ok := errors.As(res.Error, &ce)
if !ok || ce.Code != ErrCodeRateLimited {
t.Fatalf("expected rate-limit ClientError, got %v", res.Error)
}
@@ -160,7 +163,8 @@ func TestHandleChatCommandV2_NoRegistry(t *testing.T) {
result := handleChatCommandV2(context.Background(), cmd, ClientInfo{UserID: 1}, deps)
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -178,7 +182,8 @@ func TestCanPluginBroadcast_NilServiceFailsClosed(t *testing.T) {
if gate == nil {
t.Fatal("expected a forbidden Result when MessageSvc is nil")
}
ce, ok := gate.Error.(ClientError)
var ce ClientError
ok := errors.As(gate.Error, &ce)
if !ok || ce.Code != ErrCodeForbidden {
t.Errorf("expected FORBIDDEN ClientError, got %v", gate.Error)
}
+13 -6
View File
@@ -3,6 +3,7 @@ package ws
import (
"context"
"encoding/base64"
"errors"
"strings"
"testing"
@@ -153,7 +154,8 @@ func TestVoiceE2EEOfferV2_NotInVoiceChannel(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for not in voice channel")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -180,7 +182,8 @@ func TestVoiceE2EEOfferV2_EmptyFields(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for empty field")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -208,7 +211,8 @@ func TestVoiceE2EEOfferV2_OversizedFields(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for oversized field")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -236,7 +240,8 @@ func TestVoiceE2EEOfferV2_InvalidBase64(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for invalid base64")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -257,7 +262,8 @@ func TestVoiceE2EEOfferV2_NotKeyHolder(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for non-key-holder")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -276,7 +282,8 @@ func TestVoiceE2EEOfferV2_NilKeyHolder_ReturnsInternal(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for nil KeyHolder")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
+13 -6
View File
@@ -3,6 +3,7 @@ package ws
import (
"context"
"encoding/base64"
"errors"
"strings"
"testing"
)
@@ -55,7 +56,8 @@ func TestVoiceE2EEAnnounceV2_NotInVoiceChannel(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for not in voice channel")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -74,7 +76,8 @@ func TestVoiceE2EEAnnounceV2_EmptyPublicKey(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for empty public_key")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -94,7 +97,8 @@ func TestVoiceE2EEAnnounceV2_PublicKeyTooLarge(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for oversized public_key")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -113,7 +117,8 @@ func TestVoiceE2EEAnnounceV2_InvalidBase64(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for invalid base64")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -194,7 +199,8 @@ func TestVoiceE2EEAnnounceV2_SignatureInvalidBase64(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for invalid signature base64")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -217,7 +223,8 @@ func TestVoiceE2EEAnnounceV2_SignatureTooLarge(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for oversized signature")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
+11 -5
View File
@@ -3,6 +3,7 @@ package ws
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/auth"
@@ -130,7 +131,8 @@ func TestVoiceTokenRefreshV2_NotInVoice(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for not in voice")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -152,7 +154,8 @@ func TestVoiceTokenRefreshV2_RateLimited(t *testing.T) {
if result.Error == nil {
t.Fatal("expected rate limit error")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -172,7 +175,8 @@ func TestVoiceTokenRefreshV2_TokenGenNil(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error for nil TokenGen")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -192,7 +196,8 @@ func TestVoiceTokenRefreshV2_GenerateTokenError(t *testing.T) {
if result.Error == nil {
t.Fatal("expected error from GenerateToken failure")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
@@ -294,7 +299,8 @@ func TestVoiceTokenRefreshV2_RevokedConnectVoiceRefusedAndEvicts(t *testing.T) {
if result.Error == nil {
t.Fatal("revoked CONNECT_VOICE must not mint a fresh SFU token")
}
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T", result.Error)
}
+3 -1
View File
@@ -3,6 +3,7 @@ package ws
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
@@ -96,7 +97,8 @@ func (h *Hub) handleMessage(c *Client, raw []byte) {
return
}
if result.Error != nil {
if ce, ok := result.Error.(ClientError); ok {
var ce ClientError
if errors.As(result.Error, &ce) {
c.sendMsg(buildErrorMsgWithID(ce.Code, ce.Message, env.ID))
} else {
slog.Error("ws handler internal error",
+5 -2
View File
@@ -9,6 +9,7 @@ package ws
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/auth"
@@ -156,7 +157,8 @@ func TestHandleChatCommandV2_BroadcastDeniedWithoutPostPermission(t *testing.T)
result := handleChatCommandV2(context.Background(), cmd, ClientInfo{UserID: mutedID}, deps)
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T (%v)", result.Error, result.Error)
}
@@ -179,7 +181,8 @@ func TestHandleChatCommandV2_BroadcastUnknownChannel(t *testing.T) {
result := handleChatCommandV2(context.Background(), cmd, ClientInfo{UserID: ownerID}, deps)
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("expected ClientError, got %T (%v)", result.Error, result.Error)
}
+2 -3
View File
@@ -482,12 +482,11 @@ func (s *sim) read(c *simClient, n int) {
read := 0
for n < 0 || read < n {
frame, st := c.wire.Recv()
if st == ws.FaultOK {
switch st {
case ws.FaultOK:
s.observe(c, frame)
read++
continue
}
switch st {
case ws.FaultEmpty:
if len(c.owed) != c.wire.Buffered() {
s.failf("I2: c%d conn %d: owed %v but only %d unread frame(s) remain (W=%d)", c.idx, c.conns, c.owed, c.wire.Buffered(), c.w)
+3 -1
View File
@@ -11,6 +11,7 @@ package ws
import (
"context"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/db"
@@ -105,7 +106,8 @@ func TestEnableVideoSlot_SameUserDoubleStreamCountsTwoSlots(t *testing.T) {
if bobCamRes.Error == nil {
t.Fatal("bob's camera enable succeeded as the channel's 3rd live video stream against a cap of 2 -- the same-user double-publish (camera+screenshare on one row) was undercounted as a single slot")
}
if ce, ok := bobCamRes.Error.(ClientError); !ok || ce.Code != ErrCodeVideoLimit {
var ce ClientError
if !errors.As(bobCamRes.Error, &ce) || ce.Code != ErrCodeVideoLimit {
t.Errorf("error = %+v, want ClientError{Code: %q}", bobCamRes.Error, ErrCodeVideoLimit)
}
@@ -8,6 +8,7 @@ package ws
import (
"context"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/db"
@@ -97,7 +98,8 @@ func TestHandleVoiceScreenshareV2_RefusedWhenCameraSlotFull(t *testing.T) {
if ssRes.Error == nil {
t.Fatal("voice_screenshare succeeded with the channel's single video slot already held by a camera publisher — VIDEO_LIMIT was never checked")
}
if ce, ok := ssRes.Error.(ClientError); !ok || ce.Code != ErrCodeVideoLimit {
var ce ClientError
if !errors.As(ssRes.Error, &ce) || ce.Code != ErrCodeVideoLimit {
t.Errorf("error = %+v, want ClientError{Code: %q}", ssRes.Error, ErrCodeVideoLimit)
}
@@ -140,7 +142,8 @@ func TestHandleVoiceCameraV2_RefusedWhenScreenshareSlotFull(t *testing.T) {
if camRes.Error == nil {
t.Fatal("voice_camera succeeded with the channel's single video slot already held by a screenshare publisher — the slot-count query ignores screenshare rows")
}
if ce, ok := camRes.Error.(ClientError); !ok || ce.Code != ErrCodeVideoLimit {
var ce ClientError
if !errors.As(camRes.Error, &ce) || ce.Code != ErrCodeVideoLimit {
t.Errorf("error = %+v, want ClientError{Code: %q}", camRes.Error, ErrCodeVideoLimit)
}
@@ -32,11 +32,12 @@ func TestServiceErrorToResult_InternalErrorDoesNotLeakAndIsLogged(t *testing.T)
// driver error text via %v, exactly what handlers_call.go's RingTargets
// call produces when GetDMParticipantIDs fails.
driverErr := errors.New("GetDMParticipantIDs: database is locked")
svcErr := fmt.Errorf("%w: failed to read DM participants: %v", service.ErrInternal, driverErr)
svcErr := fmt.Errorf("%w: failed to read DM participants: %w", service.ErrInternal, driverErr)
result := serviceErrorToResult(svcErr)
ce, ok := result.Error.(ClientError)
var ce ClientError
ok := errors.As(result.Error, &ce)
if !ok {
t.Fatalf("serviceErrorToResult(ErrInternal wrapper) did not return a ClientError, got %T", result.Error)
}
+3 -1
View File
@@ -2,6 +2,7 @@ package ws
import (
"context"
"errors"
"fmt"
"sort"
"strings"
@@ -62,7 +63,8 @@ func TestDispatchV2_PanicIsRecovered(t *testing.T) {
if !ok {
t.Fatal("DispatchV2 must return ok=true even after a panic (handler was found)")
}
ce, isCE := result.Error.(ClientError)
var ce ClientError
isCE := errors.As(result.Error, &ce)
if !isCE {
t.Fatalf("expected ClientError after panic recovery, got %T", result.Error)
}
@@ -20,6 +20,7 @@ package ws
import (
"context"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/db"
@@ -106,7 +107,8 @@ func TestVoiceModDeafen_RollbackFollowsTargetChannelMove(t *testing.T) {
if !hookRan {
t.Fatal("voiceModDeafenPreMuteRaceHook never fired — test setup is broken, not exercising the race window")
}
clientErr, ok := result.Error.(ClientError)
var clientErr ClientError
ok := errors.As(result.Error, &clientErr)
if !ok {
t.Fatalf("result error = %#v (%T), want a ClientError", result.Error, result.Error)
}
@@ -189,7 +191,8 @@ func TestVoiceModDeafen_UndeafenRollbackDoesNotApplyOnUnauthorizedChannel(t *tes
if !hookRan {
t.Fatal("voiceModDeafenPreMuteRaceHook never fired — test setup is broken, not exercising the race window")
}
clientErr, ok := result.Error.(ClientError)
var clientErr ClientError
ok := errors.As(result.Error, &clientErr)
if !ok {
t.Fatalf("result error = %#v (%T), want a ClientError", result.Error, result.Error)
}
+5 -2
View File
@@ -10,6 +10,7 @@ package ws
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/J3vb/OwnCord/Server/auth"
@@ -108,7 +109,8 @@ func TestVoiceHandlersV2_RateLimited(t *testing.T) {
for i := range tt.limit {
res := tt.call(deps)
ce, ok := res.Error.(ClientError)
var ce ClientError
ok := errors.As(res.Error, &ce)
if !ok {
t.Fatalf("call %d: expected ClientError, got %v", i, res.Error)
}
@@ -118,7 +120,8 @@ func TestVoiceHandlersV2_RateLimited(t *testing.T) {
}
res := tt.call(deps)
ce, ok := res.Error.(ClientError)
var ce ClientError
ok := errors.As(res.Error, &ce)
if !ok {
t.Fatalf("expected ClientError past the budget, got %v", res.Error)
}