mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ Server/.env
|
||||
.claude/*
|
||||
!.claude/skills/
|
||||
!.claude/workflows/
|
||||
!.claude/rules/
|
||||
!.claude/settings.json
|
||||
CLAUDE.local.md
|
||||
.mcp.json
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user