mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
feat(auth): revocable API tokens, introspect MCP server, and a Go 1.26 idiom pass (#1266)
* feat(auth): add revocable API tokens (bot/service auth) Add long-lived, revocable API tokens so headless clients (the introspection MCP tool, bots, CI) can authenticate without a password. Presented as "Authorization: Bearer <token>", a token authenticates as a specific user, inheriting that user's role and permissions. - migration 018 + dedicated api_tokens table (kept separate from sessions so bulk logout and the per-user session cap never touch these); only the SHA-256 hash is stored, raw token shown once at creation - auth.ResolveTokenHash: one shared bearer resolver that both AuthMiddleware and adminAuthMiddleware now call. Sessions are matched first so existing login behavior is unchanged; API tokens are a fallback only on session miss. A DB outage is returned wrapped, never mistaken for a bad token. - `server token create|list|revoke` CLI: mints directly against the DB with no HTTP and no login — the password-free bootstrap path - tests: resolver (8 cases incl. outage-not-fallthrough), db queries (6), api middleware integration (valid + revoked token) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tools): add owncord-introspect MCP server A local MCP dev tool that lets Claude Code introspect a running OwnCord instance: read its logs, query any REST endpoint, and tail the desktop client's log file. It is a thin wrapper over the existing API plus the client log — no new product surface. - tools/mcp-introspect/index.mjs (Node/ESM, one dep: @modelcontextprotocol/sdk) exposes api_request (full read-write passthrough), server_logs (admin SSE ring-buffer stream), client_logs (reads the desktop log file) - authenticates with an API token (OWNCORD_API_TOKEN); pins the self-signed cert and skips hostname checks (the cert has no SAN) - registered in .mcp.json (secret-free ${OWNCORD_API_TOKEN}) - un-ignore tools/mcp-introspect/ so this shared dev tool is committed, while tools/livekit-server.exe and node_modules stay ignored - docs/mcp-introspect.md: how it works, tool reference, setup, troubleshooting Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dependencies): update and add various crate versions in Cargo.lock * feat(admin): manage API tokens from the admin panel Add Owner-gated HTTP endpoints and a UI card to create, list, and revoke API tokens from the web admin panel. Previously only the `server token` CLI could manage them, which requires shell access to the host. - POST|GET|DELETE /admin/api/tokens in admin/handlers_tokens.go, wired in admin/api.go. All three are Owner-only (ownerOnlyMiddleware, like backups/updates): an HTTP token-mint endpoint is a network-reachable credential-minting surface, and API tokens deliberately survive password change + bulk logout, so a hijacked admin session must not mint one. - Reuses the same db.*APIToken calls as the CLI; create sources the actor from request context (audits who clicked, not the bound user); the raw token is returned once in the 201 body, never stored. - Add json tags to db.APITokenListItem for snake_case wire consistency. - Admin panel: "API Tokens" nav item + create modal, show-once reveal, revoke confirm in admin/static/index.html. - Tests: 7 in admin/api_test.go (+api_tokens table in the in-memory schema). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: modernize to Go 1.26 idioms + enable modernize linter Apply `golangci-lint modernize` autofixes across the server and enable the linter in .golangci.yml so these stop re-accumulating (they built up only because modernize was never in the config). Production code: slices.Contains for hand-rolled membership loops (api router, ws origin, db/account, plugin manifest); strings.SplitSeq for allocation-free line/segment iteration (db/migrate, updater, livekit_proxy); strings.Cut (config); fmt.Appendf (dm_handler); min() (event_pruner); any (ws client). Tests: range-over-int, t.Context(), WaitGroup.Go, slices.Sort, maps.Copy, new(expr), interface{}->any. - plugin/manifest.go parent-traversal check applied by hand: modernize skipped it (two conflicting rewrites); used the slices.Contains form. - Removed the now-dead ptr() test helper after newexpr inlined its callers. - Dropped dangling sort imports left by the sort.Slice->slices.Sort rewrite. No behavior change. All four tag variants build, full test suite is green, and golangci-lint (with modernize enabled) reports 0 issues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
co-authored by
Claude Opus 4.8
parent
77ae0d21a8
commit
58005c9c6f
@@ -9,7 +9,7 @@ package ws_test
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/owncord/server/auth"
|
||||
@@ -44,7 +44,7 @@ func sortedKeys(m map[int64]bool) []int64 {
|
||||
for id := range m {
|
||||
out = append(out, id)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -55,9 +55,7 @@ type Client struct {
|
||||
// wsConn is the subset of github.com/coder/websocket.Conn used by writePump/readPump.
|
||||
// Defining it as an interface lets us avoid importing github.com/coder/websocket here,
|
||||
// keeping the core hub logic free from that dependency during unit tests.
|
||||
type wsConn interface {
|
||||
// intentionally empty — methods used only in serve.go/client_pump.go
|
||||
}
|
||||
type wsConn any
|
||||
|
||||
// newClient creates a real client wrapping a WebSocket connection (set by serve.go).
|
||||
func newClient(hub *Hub, conn wsConn, user *db.User, tokenHash string, lastSeq uint64, ctx context.Context) *Client {
|
||||
|
||||
@@ -2833,7 +2833,7 @@ func TestBroadcastToChannel_DropsWhenFull(t *testing.T) {
|
||||
hub, _ := newCoverageHub(t)
|
||||
// Don't start Run() — broadcast channel will fill up.
|
||||
// The broadcast channel capacity is 256.
|
||||
for i := 0; i < 260; i++ {
|
||||
for range 260 {
|
||||
hub.BroadcastToChannel(1, []byte(`{"type":"test"}`))
|
||||
}
|
||||
// With no Run() loop draining, some messages are dropped.
|
||||
@@ -2846,7 +2846,7 @@ func TestBroadcastToChannel_DropsWhenFull(t *testing.T) {
|
||||
|
||||
func TestBroadcastToAll_DropsWhenFull(t *testing.T) {
|
||||
hub, _ := newCoverageHub(t)
|
||||
for i := 0; i < 260; i++ {
|
||||
for range 260 {
|
||||
hub.BroadcastToAll([]byte(`{"type":"test"}`))
|
||||
}
|
||||
// Hub should still be functional after overflow — verify hub state is intact.
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestEventPersisterFlushesBatch(t *testing.T) {
|
||||
p.Start(ctx)
|
||||
t.Cleanup(func() { p.Stop(ctx) })
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
p.Enqueue(int64(i+1), "broadcast", 0, []byte(`{"type":"x"}`))
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestEventPersisterDropsOnFullQueue(t *testing.T) {
|
||||
// flusher won't drain fast enough.
|
||||
p := NewEventPersister(mem, 2, 1024, time.Hour)
|
||||
// NB: Start is intentionally NOT called so the queue stays full.
|
||||
for i := 0; i < 50; i++ {
|
||||
for i := range 50 {
|
||||
p.Enqueue(int64(i+1), "broadcast", 0, []byte(`{}`))
|
||||
}
|
||||
// Stop without Start — must not deadlock.
|
||||
@@ -95,7 +95,7 @@ func TestEventPersisterStopDrains(t *testing.T) {
|
||||
p := NewEventPersister(mem, 256, 100, time.Hour)
|
||||
p.Start(context.Background())
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := range 5 {
|
||||
p.Enqueue(int64(i+1), "broadcast", 0, []byte(`{}`))
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,7 @@ func StartEventPruner(ctx context.Context, s EventStore, retention, interval tim
|
||||
}
|
||||
// Bound the startup delay by the interval so short test intervals
|
||||
// (e.g. 100ms in event_pruner_test.go) don't wait a full minute.
|
||||
startupDelayDuration := maxStartupDelay
|
||||
if interval < startupDelayDuration {
|
||||
startupDelayDuration = interval
|
||||
}
|
||||
startupDelayDuration := min(interval, maxStartupDelay)
|
||||
go func() {
|
||||
// Run once shortly after startup so a tiny dataset stays small.
|
||||
startupDelay := time.NewTimer(startupDelayDuration)
|
||||
|
||||
@@ -105,8 +105,7 @@ func TestRunPruneErrorDoesNotPanic(t *testing.T) {
|
||||
|
||||
func TestStartEventPrunerNilStoreIsNoop(t *testing.T) {
|
||||
// Should not spawn a goroutine, should not panic.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
StartEventPruner(ctx, nil, time.Hour, time.Hour)
|
||||
// If the nil check were missing, calling PruneEventsOlderThan on nil
|
||||
// would panic inside the goroutine — but since we don't spawn one,
|
||||
@@ -145,8 +144,7 @@ func TestStartEventPrunerStartupDelayBoundedByInterval(t *testing.T) {
|
||||
// Copilot-review fix caps the startup delay at min(interval, 1min),
|
||||
// so with interval=20ms the first prune happens within ~20ms.
|
||||
s := &fakeEventStore{pruneSignal: make(chan struct{})}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
start := time.Now()
|
||||
StartEventPruner(ctx, s, time.Hour, 20*time.Millisecond)
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestHandleVoiceLeaveV2_RateLimited(t *testing.T) {
|
||||
|
||||
// voiceLeaveRateLimit (5) per voiceLeaveWindow (1s) — the 6th is rejected.
|
||||
var limited bool
|
||||
for i := 0; i < voiceLeaveRateLimit+1; i++ {
|
||||
for range voiceLeaveRateLimit + 1 {
|
||||
res := handleVoiceLeaveV2(context.Background(), cmd, info, deps)
|
||||
if res.Error != nil {
|
||||
ce, ok := res.Error.(ClientError)
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestVoiceE2EEOfferV2_RotationBurstNotRateLimited(t *testing.T) {
|
||||
// Spamming a single victim is still limited: 2 offers spent above, the
|
||||
// per-target budget is 5/sec, so within 4 more attempts one must trip.
|
||||
var limited bool
|
||||
for i := 0; i < 4; i++ {
|
||||
for range 4 {
|
||||
cmd := VoiceE2EEOfferCmd{userID: 1, targetUserID: 2, encryptedKey: validEncKey, iv: validIV}
|
||||
if result := handleVoiceE2EEOfferV2(context.Background(), cmd, info, deps); result.Error != nil {
|
||||
limited = true
|
||||
|
||||
@@ -1228,7 +1228,7 @@ func TestChatEdit_RateLimit_ReturnsError(t *testing.T) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
// Exhaust the rate limit (chatRateLimit = 10 per second).
|
||||
for i := 0; i < 11; i++ {
|
||||
for i := range 11 {
|
||||
hub.HandleMessageForTest(c, chatEditMsg(msgID, fmt.Sprintf("edit-%d", i)))
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
@@ -1822,7 +1822,6 @@ func TestPresence_InvalidStatus_ReturnsBadRequest(t *testing.T) {
|
||||
func TestPresence_ValidStatus_Broadcasts(t *testing.T) {
|
||||
validStatuses := []string{"online", "idle", "dnd", "offline"}
|
||||
for _, status := range validStatuses {
|
||||
status := status
|
||||
t.Run(status, func(t *testing.T) {
|
||||
hub, database := newHandlerHub(t)
|
||||
user := seedOwnerUser(t, database, "presence-valid-"+status)
|
||||
|
||||
+4
-5
@@ -2,6 +2,7 @@ package ws
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
@@ -23,11 +24,9 @@ func OriginAcceptOptions(allowedOrigins []string) *websocket.AcceptOptions {
|
||||
return &websocket.AcceptOptions{InsecureSkipVerify: false}
|
||||
}
|
||||
|
||||
for _, o := range allowedOrigins {
|
||||
if o == "*" {
|
||||
slog.Warn("ws: allowed_origins contains wildcard '*' — accepting connections from ANY origin (insecure)")
|
||||
return &websocket.AcceptOptions{InsecureSkipVerify: true}
|
||||
}
|
||||
if slices.Contains(allowedOrigins, "*") {
|
||||
slog.Warn("ws: allowed_origins contains wildcard '*' — accepting connections from ANY origin (insecure)")
|
||||
return &websocket.AcceptOptions{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
return &websocket.AcceptOptions{
|
||||
|
||||
@@ -2,7 +2,7 @@ package ws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -222,7 +222,7 @@ func TestPubSub_TopicsForClient(t *testing.T) {
|
||||
ps.Subscribe(c, UserTopic(1))
|
||||
|
||||
topics := ps.TopicsForClient(1)
|
||||
sort.Slice(topics, func(i, j int) bool { return topics[i] < topics[j] })
|
||||
slices.Sort(topics)
|
||||
|
||||
expected := []Topic{"channel:10", TopicGlobal, UserTopic(1)}
|
||||
if len(topics) != len(expected) {
|
||||
@@ -264,7 +264,7 @@ func TestPubSub_ConcurrentAccess(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(N * 3) // subscribe + publish + unsubscribe
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
for i := range N {
|
||||
c := makeTestClient(int64(i))
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestReconnect_BufferMiss_FallsBackToDBTier(t *testing.T) {
|
||||
eventStore := openEventStoreDB(t)
|
||||
bgCtx := context.Background()
|
||||
for seq := int64(501); seq <= 600; seq++ {
|
||||
payload := []byte(fmt.Sprintf(`{"seq":%d,"type":"broadcast"}`, seq))
|
||||
payload := fmt.Appendf(nil, `{"seq":%d,"type":"broadcast"}`, seq)
|
||||
if err := eventStore.PersistEvent(bgCtx, seq, "broadcast", 0, payload); err != nil {
|
||||
t.Fatalf("PersistEvent seq=%d: %v", seq, err)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestPush_SingleEntry(t *testing.T) {
|
||||
func TestPush_MultipleInOrder(t *testing.T) {
|
||||
rb := ws.NewEventRingBuffer(8)
|
||||
for i := uint64(1); i <= 5; i++ {
|
||||
rb.Push(i, 0, []byte(fmt.Sprintf("msg-%d", i)))
|
||||
rb.Push(i, 0, fmt.Appendf(nil, "msg-%d", i))
|
||||
}
|
||||
|
||||
// afterSeq == oldestSeq (1) → nil (BUG-085: conservative boundary).
|
||||
@@ -60,7 +60,7 @@ func TestPush_WrapsAround(t *testing.T) {
|
||||
|
||||
// Push 6 events into a buffer with capacity 4 — first two are evicted.
|
||||
for i := uint64(1); i <= 6; i++ {
|
||||
rb.Push(i, 0, []byte(fmt.Sprintf("e%d", i)))
|
||||
rb.Push(i, 0, fmt.Appendf(nil, "e%d", i))
|
||||
}
|
||||
|
||||
got := rb.EventsSince(0)
|
||||
@@ -139,7 +139,7 @@ func TestEventsSince_EmptyBuffer(t *testing.T) {
|
||||
func TestEventsSince_AfterSpecificSeq(t *testing.T) {
|
||||
rb := ws.NewEventRingBuffer(8)
|
||||
for i := uint64(1); i <= 5; i++ {
|
||||
rb.Push(i, 0, []byte(fmt.Sprintf("m%d", i)))
|
||||
rb.Push(i, 0, fmt.Appendf(nil, "m%d", i))
|
||||
}
|
||||
|
||||
got := rb.EventsSince(3)
|
||||
@@ -185,7 +185,7 @@ func TestEventsSince_WraparoundOrder(t *testing.T) {
|
||||
|
||||
// Fill past capacity to force wrap.
|
||||
for i := uint64(1); i <= 7; i++ {
|
||||
rb.Push(i, 0, []byte(fmt.Sprintf("v%d", i)))
|
||||
rb.Push(i, 0, fmt.Appendf(nil, "v%d", i))
|
||||
}
|
||||
|
||||
// afterSeq == oldestSeq (4) → nil (BUG-085).
|
||||
@@ -211,7 +211,7 @@ func TestEventsSince_AfterSeqZero_ReturnsBehavior(t *testing.T) {
|
||||
// the server can't confirm the buffer covers everything the client missed.
|
||||
rb := ws.NewEventRingBuffer(8)
|
||||
for i := uint64(1); i <= 3; i++ {
|
||||
rb.Push(i, 0, []byte(fmt.Sprintf("a%d", i)))
|
||||
rb.Push(i, 0, fmt.Appendf(nil, "a%d", i))
|
||||
}
|
||||
|
||||
got := rb.EventsSince(0)
|
||||
@@ -250,7 +250,7 @@ func TestEventsSince_AfterSeqEqualsOldest_ReturnsNil(t *testing.T) {
|
||||
|
||||
// Push 6 events: buffer holds seq 3,4,5,6. Oldest = 3.
|
||||
for i := uint64(1); i <= 6; i++ {
|
||||
rb.Push(i, 0, []byte(fmt.Sprintf("e%d", i)))
|
||||
rb.Push(i, 0, fmt.Appendf(nil, "e%d", i))
|
||||
}
|
||||
|
||||
if oldest := rb.OldestSeq(); oldest != 3 {
|
||||
@@ -317,26 +317,24 @@ func TestConcurrent_PushAndEventsSince(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrent writers.
|
||||
for w := 0; w < writers; w++ {
|
||||
for w := range writers {
|
||||
wg.Add(1)
|
||||
go func(base uint64) {
|
||||
defer wg.Done()
|
||||
for i := uint64(0); i < pushes; i++ {
|
||||
for i := range uint64(pushes) {
|
||||
rb.Push(base+i, 0, []byte("data"))
|
||||
}
|
||||
}(uint64(w) * pushes)
|
||||
}
|
||||
|
||||
// Concurrent readers.
|
||||
for r := 0; r < readers; r++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < reads; i++ {
|
||||
for range readers {
|
||||
wg.Go(func() {
|
||||
for range reads {
|
||||
_ = rb.EventsSince(0)
|
||||
_ = rb.OldestSeq()
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -438,7 +436,7 @@ func TestEventsSince_CapacityBoundaries(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rb := ws.NewEventRingBuffer(tc.cap)
|
||||
for i := 1; i <= tc.pushes; i++ {
|
||||
rb.Push(uint64(i), 0, []byte(fmt.Sprintf("e%d", i)))
|
||||
rb.Push(uint64(i), 0, fmt.Appendf(nil, "e%d", i))
|
||||
}
|
||||
|
||||
got := rb.EventsSince(tc.afterSeq)
|
||||
|
||||
@@ -455,12 +455,12 @@ func TestE2EE_ConcurrentPubKeyAccess(t *testing.T) {
|
||||
// Concurrent set/get of e2eePubKey should not race.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := range 100 {
|
||||
ws.SetClientE2EEPubKeyForTest(c, "key-"+string(rune('A'+i%26)))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
_ = ws.GetClientE2EEPubKeyForTest(c)
|
||||
}
|
||||
<-done
|
||||
|
||||
@@ -485,7 +485,7 @@ func TestServeWS_DuplicateLogin_KeepsUserOnline(t *testing.T) {
|
||||
if writeErr := conn.Write(ctx, websocket.MessageText, raw); writeErr != nil {
|
||||
t.Fatalf("write auth: %v", writeErr)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
if _, _, readErr := conn.Read(ctx); readErr != nil {
|
||||
t.Fatalf("read handshake message %d: %v", i, readErr)
|
||||
}
|
||||
@@ -575,7 +575,7 @@ func TestServeWS_Reconnect_PreservesVoiceState(t *testing.T) {
|
||||
t.Fatalf("write auth: %v", writeErr)
|
||||
}
|
||||
// Read auth_ok + ready
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
if _, _, readErr := conn.Read(ctx); readErr != nil {
|
||||
t.Fatalf("read handshake message %d: %v", i, readErr)
|
||||
}
|
||||
@@ -731,7 +731,7 @@ func TestServeWS_Reconnect_AuthorizedVoiceClientKeepsChannelStream(t *testing.T)
|
||||
t.Fatalf("write auth: %v", writeErr)
|
||||
}
|
||||
// Read auth_ok + first following message.
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
if _, _, readErr := conn.Read(ctx); readErr != nil {
|
||||
t.Fatalf("read handshake message %d: %v", i, readErr)
|
||||
}
|
||||
@@ -884,7 +884,7 @@ func TestServeWS_FreshReconnect_CleansStaleVoiceState(t *testing.T) {
|
||||
t.Fatalf("write auth: %v", writeErr)
|
||||
}
|
||||
// Read auth_ok + ready
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
if _, _, readErr := conn.Read(ctx); readErr != nil {
|
||||
t.Fatalf("read handshake message %d: %v", i, readErr)
|
||||
}
|
||||
@@ -1085,7 +1085,7 @@ func TestServeWS_writePump_MessageDelivered(t *testing.T) {
|
||||
_ = conn.Write(ctx, websocket.MessageText, raw)
|
||||
|
||||
// Drain auth_ok and ready.
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
_, _, err := conn.Read(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("drain initial messages: %v", err)
|
||||
@@ -1197,7 +1197,7 @@ func TestIntegration_MessageRoundTrip(t *testing.T) {
|
||||
t.Fatalf("%s write auth: %v", label, writeErr)
|
||||
}
|
||||
// Drain auth_ok + ready.
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
if _, _, readErr := conn.Read(ctx); readErr != nil {
|
||||
t.Fatalf("%s drain initial msg %d: %v", label, i, readErr)
|
||||
}
|
||||
@@ -1325,7 +1325,7 @@ func TestIntegration_SequenceNumbers(t *testing.T) {
|
||||
t.Fatalf("write auth: %v", err)
|
||||
}
|
||||
// Drain auth_ok and ready (these are direct writes, not broadcasts).
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
if _, _, err := conn.Read(ctx); err != nil {
|
||||
t.Fatalf("drain msg %d: %v", i, err)
|
||||
}
|
||||
@@ -1342,7 +1342,7 @@ func TestIntegration_SequenceNumbers(t *testing.T) {
|
||||
var seqs []float64
|
||||
readCtx, readCancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer readCancel()
|
||||
for i := 0; i < 10; i++ {
|
||||
for range 10 {
|
||||
_, raw, readErr := conn.Read(readCtx)
|
||||
if readErr != nil {
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user