package message import ( "context" "database/sql" "encoding/json" "testing" "time" "git.asio.asia/nixevol/NixMsg/internal/app/port" "git.asio.asia/nixevol/NixMsg/internal/protocol" ) // 定时到点且接收方离线成终态时,在线发送方经 WakePush 拿到回执。 func TestR365DispatchDueWakesSenderForReceipt(t *testing.T) { t.Parallel() e := openDeliveryEnv(t, nil) insertEndpoint(t, e.db, "alice", "", 1, 0) insertEndpoint(t, e.db, "bob", "", 1, 0) _ = e.db.Queue.Do(context.Background(), func(tx *sql.Tx) error { _, err := tx.Exec(`UPDATE endpoints SET offline_since = ? WHERE id=?`, e.nowMs-120_000, "bob") return err }) ctx := context.Background() aliceConn := LiveConn{ConnID: "c-alice", Ready: true} e.conns.Set("alice", aliceConn) if err := e.app.OnHandshakeComplete(ctx, "alice", aliceConn); err != nil { t.Fatal(err) } t.Cleanup(func() { e.app.stopPushWorker("alice", "") }) delay := int64(5000) req := baseSend("due-rcpt", "bob") req.DelayMs = &delay if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil { t.Fatal(err) } st, _ := e.msgState("alice", "due-rcpt") if st != StateScheduled { t.Fatalf("want scheduled got %s", st) } if e.down.FilterType(protocol.TypeReceipt) != 0 { t.Fatal("receipt before due") } e.setNow(e.nowMs + 5000) if _, err := e.app.DispatchDue(ctx, e.nowMs, 10); err != nil { t.Fatal(err) } deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if e.down.FilterType(protocol.TypeReceipt) > 0 { return } time.Sleep(20 * time.Millisecond) } t.Fatalf("sender got no receipt after scheduled dispatch, receipts=%d", e.down.FilterType(protocol.TypeReceipt)) } // 窗口内首条超限拒收后,同连接后续小消息仍被 worker 推送。 func TestR365TooLargeThenSmallContinuesPush(t *testing.T) { t.Parallel() e := openDeliveryEnv(t, func(l *Limits) { l.DeliveryWindow = 1 }) insertEndpoint(t, e.db, "alice", "", 1, 0) insertEndpoint(t, e.db, "bob", "", 1, 0) ctx := context.Background() // 整帧上限:小消息能过,大正文整帧超限被拒。 bobConn := LiveConn{ConnID: "c-bob", Ready: true, MaxReceiveBytes: 200} e.conns.Set("bob", bobConn) if err := e.app.OnHandshakeComplete(ctx, "bob", bobConn); err != nil { t.Fatal(err) } t.Cleanup(func() { e.app.stopPushWorker("bob", "") }) big := baseSend("big-skip", "bob") b := make([]byte, 400) for i := range b { b[i] = 'A' } big.Body.Data = string(b) if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, big); err != nil { t.Fatal(err) } small := baseSend("small-ok", "bob") small.Body.Data = "hi" if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, small); err != nil { t.Fatal(err) } deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { foundSmall := false for _, p := range e.down.Snapshots() { if payloadType(p.Payload) != protocol.TypeMsg { continue } var m struct { ID string `json:"id"` } _ = json.Unmarshal(p.Payload, &m) if m.ID == "small-ok" { foundSmall = true } } if foundSmall { seqBig := e.seqOf("alice", "big-skip") st, reason := e.deliveryState(seqBig, "bob") if st != DeliveryRejected || reason != ReasonTooLarge { t.Fatalf("big: %s/%s", st, reason) } return } time.Sleep(20 * time.Millisecond) } t.Fatalf("small msg not pushed after too_large; msgs=%d snapshots=%d", e.down.FilterType(protocol.TypeMsg), len(e.down.Snapshots())) } // 撤回已推在途消息后,同连接更晚的 pending 继续推。 func TestR365RecallFreesWindowForLaterPending(t *testing.T) { t.Parallel() e := openDeliveryEnv(t, func(l *Limits) { l.DeliveryWindow = 1 }) insertEndpoint(t, e.db, "alice", "", 1, 0) insertEndpoint(t, e.db, "bob", "", 1, 0) ctx := context.Background() bobConn := LiveConn{ConnID: "c-bob", Ready: true} e.conns.Set("bob", bobConn) if err := e.app.OnHandshakeComplete(ctx, "bob", bobConn); err != nil { t.Fatal(err) } t.Cleanup(func() { e.app.stopPushWorker("bob", "") }) if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("first-in-flight", "bob")); err != nil { t.Fatal(err) } if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("second-wait", "bob")); err != nil { t.Fatal(err) } deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if e.down.FilterType(protocol.TypeMsg) >= 1 { break } time.Sleep(10 * time.Millisecond) } if e.down.FilterType(protocol.TypeMsg) < 1 { t.Fatal("first msg not pushed") } seq1 := e.seqOf("alice", "first-in-flight") var pushed sql.NullString _ = e.db.Read.QueryRow(`SELECT pushed_conn FROM deliveries WHERE seq=? AND endpoint_id='bob'`, seq1).Scan(&pushed) if !pushed.Valid { t.Fatal("first not in-flight") } if _, err := e.app.Recall(ctx, "alice", &protocol.Recall{ V: protocol.Version, Type: protocol.TypeRecall, RID: "r1", ID: "first-in-flight", }); err != nil { t.Fatal(err) } deadline = time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { for _, p := range e.down.Snapshots() { if payloadType(p.Payload) != protocol.TypeMsg { continue } var m struct { ID string `json:"id"` } _ = json.Unmarshal(p.Payload, &m) if m.ID == "second-wait" { return } } time.Sleep(20 * time.Millisecond) } t.Fatalf("second pending not pushed after recall; msgs=%d", e.down.FilterType(protocol.TypeMsg)) }