745 lines
23 KiB
Go
745 lines
23 KiB
Go
package message
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.asio.asia/nixevol/NixMsg/internal/app/port"
|
|
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
|
"git.asio.asia/nixevol/NixMsg/internal/protocol"
|
|
"git.asio.asia/nixevol/NixMsg/internal/store"
|
|
)
|
|
|
|
type deliveryEnv struct {
|
|
t *testing.T
|
|
app *App
|
|
db *store.DB
|
|
conns *MemoryConns
|
|
down *RecordingDownlink
|
|
nowMs int64
|
|
}
|
|
|
|
func openDeliveryEnv(t *testing.T, mutate func(*Limits)) *deliveryEnv {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
db, err := store.Open(filepath.Join(dir, "data"), "FULL")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
lim := defaultTestLimits()
|
|
lim.GraceSeconds = 60
|
|
lim.AckTimeoutSeconds = 300
|
|
lim.DeliveryWindow = 32
|
|
if mutate != nil {
|
|
mutate(&lim)
|
|
}
|
|
nowMs := int64(1_700_000_000_000)
|
|
conns := NewMemoryConns()
|
|
down := &RecordingDownlink{}
|
|
app := New(db, lim, auth.NewStubHashPool(),
|
|
WithNow(func() time.Time { return time.UnixMilli(nowMs) }),
|
|
WithLocks(auth.NewStubLoginLocks()),
|
|
WithConnRegistry(conns),
|
|
WithDownlink(down),
|
|
)
|
|
return &deliveryEnv{t: t, app: app, db: db, conns: conns, down: down, nowMs: nowMs}
|
|
}
|
|
|
|
func (e *deliveryEnv) setNow(ms int64) {
|
|
e.nowMs = ms
|
|
e.app.nowFn = func() time.Time { return time.UnixMilli(e.nowMs) }
|
|
}
|
|
|
|
func (e *deliveryEnv) online(id string, connID port.ConnID) {
|
|
e.conns.Set(id, LiveConn{ConnID: connID, MaxReceiveBytes: 0, MaxPacketSize: 0})
|
|
}
|
|
|
|
func (e *deliveryEnv) deliveryState(seq int64, endpointID string) (state, reason string) {
|
|
e.t.Helper()
|
|
err := e.db.Read.QueryRow(`SELECT state, reason FROM deliveries WHERE seq=? AND endpoint_id=?`, seq, endpointID).Scan(&state, &reason)
|
|
if err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (e *deliveryEnv) msgState(sender, id string) (state, reason string) {
|
|
e.t.Helper()
|
|
err := e.db.Read.QueryRow(`SELECT state, reason FROM messages WHERE sender_id=? AND id=?`, sender, id).Scan(&state, &reason)
|
|
if err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (e *deliveryEnv) seqOf(sender, id string) int64 {
|
|
e.t.Helper()
|
|
var seq int64
|
|
if err := e.db.Read.QueryRow(`SELECT seq FROM messages WHERE sender_id=? AND id=?`, sender, id).Scan(&seq); err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
return seq
|
|
}
|
|
|
|
func keepTrue() *protocol.OfflineOpts {
|
|
ttl := int64(3600)
|
|
return &protocol.OfflineOpts{Keep: true, TTLSeconds: &ttl}
|
|
}
|
|
|
|
func TestDeliveryStateMachine(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("F10_grace_within_keeps_pending", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
// offline_since = now → 宽限内
|
|
ctx := context.Background()
|
|
res, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("g1", "bob"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.State != StateDispatched {
|
|
t.Fatalf("state=%s", res.State)
|
|
}
|
|
seq := e.seqOf("alice", "g1")
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryPending || reason != "" {
|
|
t.Fatalf("got %s/%s", st, reason)
|
|
}
|
|
var exp sql.NullInt64
|
|
_ = e.db.Read.QueryRow(`SELECT expire_at FROM deliveries WHERE seq=?`, seq).Scan(&exp)
|
|
if !exp.Valid || exp.Int64 != e.nowMs+60_000 {
|
|
t.Fatalf("expire_at=%v want %d", exp, e.nowMs+60_000)
|
|
}
|
|
})
|
|
|
|
t.Run("F10_grace_exceeded_dropped_offline", func(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()
|
|
res, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("g2", "bob"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.State != StateCompleted {
|
|
t.Fatalf("state=%s", res.State)
|
|
}
|
|
seq := e.seqOf("alice", "g2")
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryDropped || reason != ReasonOffline {
|
|
t.Fatalf("got %s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("F09_keep_ttl_expire_via_cleanup", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, func(l *Limits) { l.GraceSeconds = 60 })
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
req := baseSend("k1", "bob")
|
|
req.Offline = keepTrue()
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq := e.seqOf("alice", "k1")
|
|
st, _ := e.deliveryState(seq, "bob")
|
|
if st != DeliveryPending {
|
|
t.Fatalf("state=%s", st)
|
|
}
|
|
e.setNow(e.nowMs + 3600_000 + 1)
|
|
if err := e.app.CleanupOnce(ctx, e.nowMs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryExpired || reason != ReasonTTL {
|
|
t.Fatalf("got %s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("F08_ack_timeout_not_keep_dropped", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, func(l *Limits) { l.AckTimeoutSeconds = 10 })
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("a1", "bob")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if e.down.FilterType(protocol.TypeMsg) < 1 {
|
|
t.Fatal("expected msg publish")
|
|
}
|
|
seq := e.seqOf("alice", "a1")
|
|
var pushed sql.NullString
|
|
_ = e.db.Read.QueryRow(`SELECT pushed_conn FROM deliveries WHERE seq=?`, seq).Scan(&pushed)
|
|
if !pushed.Valid {
|
|
t.Fatal("expected pushed")
|
|
}
|
|
e.setNow(e.nowMs + 11_000)
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryDropped || reason != ReasonNotAcked {
|
|
t.Fatalf("got %s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("F08_ack_timeout_keep_repush_then_expire", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, func(l *Limits) {
|
|
l.AckTimeoutSeconds = 10
|
|
})
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
req := baseSend("a2", "bob")
|
|
ttl := int64(30)
|
|
req.Offline = &protocol.OfflineOpts{Keep: true, TTLSeconds: &ttl}
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq := e.seqOf("alice", "a2")
|
|
// 未过 expire:清标记重推
|
|
e.setNow(e.nowMs + 11_000)
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, _ := e.deliveryState(seq, "bob")
|
|
if st != DeliveryPending {
|
|
t.Fatalf("want pending got %s", st)
|
|
}
|
|
var pushed sql.NullString
|
|
_ = e.db.Read.QueryRow(`SELECT pushed_conn FROM deliveries WHERE seq=?`, seq).Scan(&pushed)
|
|
// 可能已重推或仍清空后待推
|
|
// 过 expire
|
|
e.setNow(e.nowMs + 30_000)
|
|
_ = e.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`UPDATE deliveries SET pushed_conn=?, pushed_at=? WHERE seq=?`, "c-bob", e.nowMs-11_000, seq)
|
|
return err
|
|
})
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryExpired || reason != ReasonTTL {
|
|
t.Fatalf("got %s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("F12_scheduled_recall", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
delay := int64(10_000)
|
|
req := baseSend("r1", "bob")
|
|
req.DelayMs = &delay
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := e.app.Recall(ctx, "alice", &protocol.Recall{V: 1, Type: protocol.TypeRecall, RID: "1", ID: "r1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data.Result != "recalled" || data.Recalled != 0 {
|
|
t.Fatalf("%+v", data)
|
|
}
|
|
st, reason := e.msgState("alice", "r1")
|
|
if st != StateCompleted || reason != ReasonRecalled {
|
|
t.Fatalf("%s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("F13_recall_race_partial", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
insertEndpoint(t, e.db, "carol", "", 1, 0)
|
|
ctx := context.Background()
|
|
_ = e.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`INSERT INTO groups(id, name, owner_id, created_at) VALUES('g1','g','alice',?)`, e.nowMs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, m := range []string{"alice", "bob", "carol"} {
|
|
if _, err := tx.Exec(`INSERT INTO group_members(group_id, endpoint_id, joined_at) VALUES('g1',?,?)`, m, e.nowMs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
req := &protocol.Send{
|
|
V: protocol.Version, Type: protocol.TypeSend, RID: "1", ID: "grp1",
|
|
To: protocol.Target{Kind: protocol.TargetGroup, ID: "g1"},
|
|
Body: protocol.Body{Enc: protocol.EncUTF8, Data: "hi"},
|
|
}
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq := e.seqOf("alice", "grp1")
|
|
// bob 先确认
|
|
if _, err := e.app.Ack(ctx, "bob", &protocol.Ack{V: 1, Type: protocol.TypeAck, RID: "a", From: "alice", ID: "grp1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := e.app.Recall(ctx, "alice", &protocol.Recall{V: 1, Type: protocol.TypeRecall, RID: "2", ID: "grp1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data.Result != "partial" || data.Accepted != 1 || data.Recalled != 1 {
|
|
t.Fatalf("%+v", data)
|
|
}
|
|
st, _ := e.deliveryState(seq, "carol")
|
|
if st != DeliveryRecalled {
|
|
t.Fatalf("carol=%s", st)
|
|
}
|
|
st, _ = e.deliveryState(seq, "bob")
|
|
if st != DeliveryAccepted {
|
|
t.Fatalf("bob=%s", st)
|
|
}
|
|
})
|
|
|
|
t.Run("F13_recall_pending_unpushed", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
ctx := context.Background()
|
|
req := baseSend("r2", "bob")
|
|
req.Offline = keepTrue()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := e.app.Recall(ctx, "alice", &protocol.Recall{V: 1, Type: protocol.TypeRecall, RID: "1", ID: "r2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data.Result != "recalled" || data.Recalled != 1 {
|
|
t.Fatalf("%+v", data)
|
|
}
|
|
})
|
|
|
|
t.Run("F14_receipt_when_sender_offline", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("rc1", "bob")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := e.app.Ack(ctx, "bob", &protocol.Ack{V: 1, Type: protocol.TypeAck, RID: "a", From: "alice", ID: "rc1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var n int
|
|
if err := e.db.Read.QueryRow(`SELECT COUNT(*) FROM receipts WHERE sender_id=? AND msg_id=? AND state=?`, "alice", "rc1", DeliveryAccepted).Scan(&n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("receipts=%d", n)
|
|
}
|
|
// alice 上线后能推到回执
|
|
e.online("alice", "c-alice")
|
|
if err := e.app.PushPending(ctx, "alice", "c-alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if e.down.FilterType(protocol.TypeReceipt) < 1 {
|
|
t.Fatal("expected receipt push")
|
|
}
|
|
})
|
|
|
|
t.Run("F18_body_gone_record_zero_idempotent_kept", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, func(l *Limits) { l.RecordRetentionDays = 0 })
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
req := baseSend("f18", "bob")
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := e.app.Ack(ctx, "bob", &protocol.Ack{V: 1, Type: protocol.TypeAck, RID: "a", From: "alice", ID: "f18"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var bodies int
|
|
_ = e.db.Read.QueryRow(`SELECT COUNT(*) FROM message_bodies`).Scan(&bodies)
|
|
if bodies != 0 {
|
|
t.Fatalf("bodies=%d", bodies)
|
|
}
|
|
var msgs int
|
|
_ = e.db.Read.QueryRow(`SELECT COUNT(*) FROM messages WHERE id=?`, "f18").Scan(&msgs)
|
|
if msgs != 0 {
|
|
t.Fatalf("messages=%d want 0", msgs)
|
|
}
|
|
var keys int
|
|
_ = e.db.Read.QueryRow(`SELECT COUNT(*) FROM send_keys WHERE msg_id=?`, "f18").Scan(&keys)
|
|
if keys != 1 {
|
|
t.Fatalf("send_keys=%d", keys)
|
|
}
|
|
// 防重:消息已删 → not_found
|
|
_, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req)
|
|
if protoCode(err) != protocol.CodeNotFound {
|
|
t.Fatalf("want not_found got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("F11_dispatch_due_after_downtime", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
delay := int64(60_000)
|
|
req := baseSend("due1", "bob")
|
|
req.DelayMs = &delay
|
|
ctx := context.Background()
|
|
res, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.State != StateScheduled {
|
|
t.Fatalf("%s", res.State)
|
|
}
|
|
e.setNow(e.nowMs + 60_000)
|
|
n, err := e.app.DispatchDue(ctx, e.nowMs, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("dispatched=%d", n)
|
|
}
|
|
st, _ := e.msgState("alice", "due1")
|
|
if st != StateDispatched {
|
|
t.Fatalf("%s", st)
|
|
}
|
|
})
|
|
|
|
t.Run("F10_recover_clears_pushed_and_extends_grace", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
req := baseSend("rec1", "bob")
|
|
req.Offline = keepTrue()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq := e.seqOf("alice", "rec1")
|
|
// 模拟保留期在停机期间已过
|
|
_ = e.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`UPDATE deliveries SET expire_at=?, pushed_conn=? WHERE seq=?`, e.nowMs-1000, "old-conn", seq)
|
|
return err
|
|
})
|
|
_ = e.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`UPDATE endpoints SET online_since=?, offline_since=NULL WHERE id=?`, e.nowMs-5000, "bob")
|
|
return err
|
|
})
|
|
start := e.nowMs + 1_000
|
|
e.setNow(start)
|
|
if err := e.app.RecoverOnStart(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pushed sql.NullString
|
|
var exp sql.NullInt64
|
|
_ = e.db.Read.QueryRow(`SELECT pushed_conn, expire_at FROM deliveries WHERE seq=?`, seq).Scan(&pushed, &exp)
|
|
if pushed.Valid {
|
|
t.Fatalf("pushed_conn still set: %v", pushed.String)
|
|
}
|
|
wantMin := start + 60_000
|
|
if !exp.Valid || exp.Int64 < wantMin {
|
|
t.Fatalf("expire_at=%v want >= %d", exp, wantMin)
|
|
}
|
|
var offline sql.NullInt64
|
|
_ = e.db.Read.QueryRow(`SELECT offline_since FROM endpoints WHERE id=?`, "bob").Scan(&offline)
|
|
if !offline.Valid || offline.Int64 != start {
|
|
t.Fatalf("offline_since=%v want %d", offline, start)
|
|
}
|
|
})
|
|
|
|
t.Run("disabled_target_rejected", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
delay := int64(5_000)
|
|
req := baseSend("dis1", "bob")
|
|
req.DelayMs = &delay
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = e.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`UPDATE endpoints SET enabled=0 WHERE id=?`, "bob")
|
|
return err
|
|
})
|
|
e.setNow(e.nowMs + 5_000)
|
|
if _, err := e.app.DispatchDue(ctx, e.nowMs, 10); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq := e.seqOf("alice", "dis1")
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryRejected || reason != ReasonEndpointDisabled {
|
|
t.Fatalf("%s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("queue_full_rejected", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, func(l *Limits) { l.MaxPendingPerReceiver = 1 })
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
req1 := baseSend("qf1", "bob")
|
|
req1.Offline = keepTrue()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req2 := baseSend("qf2", "bob")
|
|
req2.Offline = keepTrue()
|
|
res, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.State != StateCompleted {
|
|
t.Fatalf("state=%s", res.State)
|
|
}
|
|
seq := e.seqOf("alice", "qf2")
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryRejected || reason != ReasonQueueFull {
|
|
t.Fatalf("%s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("sender_left_no_recipients", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
ctx := context.Background()
|
|
_ = e.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`INSERT INTO groups(id,name,owner_id,created_at) VALUES('g2','g','alice',?)`, e.nowMs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = tx.Exec(`INSERT INTO group_members(group_id,endpoint_id,joined_at) VALUES('g2','alice',?)`, e.nowMs)
|
|
return err
|
|
})
|
|
delay := int64(1000)
|
|
req := &protocol.Send{
|
|
V: protocol.Version, Type: protocol.TypeSend, RID: "1", ID: "nr1",
|
|
To: protocol.Target{Kind: protocol.TargetGroup, ID: "g2"},
|
|
Body: protocol.Body{Enc: protocol.EncUTF8, Data: "x"}, DelayMs: &delay,
|
|
}
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e.setNow(e.nowMs + 1000)
|
|
if _, err := e.app.DispatchDue(ctx, e.nowMs, 10); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, reason := e.msgState("alice", "nr1")
|
|
if st != StateCompleted || reason != ReasonNoRecipients {
|
|
t.Fatalf("%s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("too_large_rejected", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.conns.Set("bob", LiveConn{ConnID: "c-bob", MaxReceiveBytes: 50})
|
|
ctx := context.Background()
|
|
req := baseSend("big1", "bob")
|
|
req.Body.Data = string(make([]byte, 200))
|
|
for i := range req.Body.Data {
|
|
// utf8 valid
|
|
_ = i
|
|
}
|
|
req.Body.Data = "{\"x\":\"" + string(make([]byte, 80)) + "\"}"
|
|
// simpler: long ascii
|
|
b := make([]byte, 80)
|
|
for i := range b {
|
|
b[i] = 'a'
|
|
}
|
|
req.Body.Data = string(b)
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq := e.seqOf("alice", "big1")
|
|
st, reason := e.deliveryState(seq, "bob")
|
|
if st != DeliveryRejected || reason != ReasonTooLarge {
|
|
t.Fatalf("%s/%s", st, reason)
|
|
}
|
|
})
|
|
|
|
t.Run("on_publish_dropped_clears_mark", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("drop1", "bob")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := e.app.PushPending(ctx, "bob", "c-bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
payload := e.down.Snapshots()[0].Payload
|
|
if err := e.app.OnPublishDropped(ctx, "bob", "c-bob", payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq := e.seqOf("alice", "drop1")
|
|
var pushed sql.NullString
|
|
_ = e.db.Read.QueryRow(`SELECT pushed_conn FROM deliveries WHERE seq=?`, seq).Scan(&pushed)
|
|
if pushed.Valid {
|
|
t.Fatalf("still pushed %s", pushed.String)
|
|
}
|
|
})
|
|
|
|
t.Run("ack_duplicate_and_terminal", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("ack1", "bob")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = e.app.PushPending(ctx, "bob", "c-bob")
|
|
r1, err := e.app.Ack(ctx, "bob", &protocol.Ack{V: 1, Type: protocol.TypeAck, RID: "1", From: "alice", ID: "ack1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r1.Result != DeliveryAccepted {
|
|
t.Fatalf("%+v", r1)
|
|
}
|
|
r2, err := e.app.Ack(ctx, "bob", &protocol.Ack{V: 1, Type: protocol.TypeAck, RID: "2", From: "alice", ID: "ack1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r2.Result != DeliveryAccepted {
|
|
t.Fatalf("dup %+v", r2)
|
|
}
|
|
})
|
|
|
|
t.Run("disconnect_extends_grace", func(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("dc1", "bob")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = e.app.PushPending(ctx, "bob", "c-bob")
|
|
if err := e.app.OnDisconnect(ctx, "bob", "c-bob", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e.conns.Clear("bob", "c-bob")
|
|
seq := e.seqOf("alice", "dc1")
|
|
var pushed sql.NullString
|
|
var exp sql.NullInt64
|
|
_ = e.db.Read.QueryRow(`SELECT pushed_conn, expire_at FROM deliveries WHERE seq=?`, seq).Scan(&pushed, &exp)
|
|
if pushed.Valid {
|
|
t.Fatal("pushed should clear")
|
|
}
|
|
if !exp.Valid || exp.Int64 != e.nowMs+60_000 {
|
|
t.Fatalf("expire_at=%v", exp)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRecallDoesNotWriteReceipt(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
ctx := context.Background()
|
|
req := baseSend("nr", "bob")
|
|
req.Offline = keepTrue()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := e.app.Recall(ctx, "alice", &protocol.Recall{V: 1, Type: protocol.TypeRecall, RID: "1", ID: "nr"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var n int
|
|
_ = e.db.Read.QueryRow(`SELECT COUNT(*) FROM receipts WHERE msg_id=?`, "nr").Scan(&n)
|
|
if n != 0 {
|
|
t.Fatalf("receipts=%d", n)
|
|
}
|
|
}
|
|
|
|
func TestPushRevokedOnRecallAfterPush(t *testing.T) {
|
|
t.Parallel()
|
|
e := openDeliveryEnv(t, nil)
|
|
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
e.online("bob", "c-bob")
|
|
ctx := context.Background()
|
|
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("rv1", "bob")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = e.app.PushPending(ctx, "bob", "c-bob")
|
|
if _, err := e.app.Recall(ctx, "alice", &protocol.Recall{V: 1, Type: protocol.TypeRecall, RID: "1", ID: "rv1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, p := range e.down.Snapshots() {
|
|
var head struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(p.Payload, &head)
|
|
if head.Type == protocol.TypeRevoked {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("expected revoked frame")
|
|
}
|
|
}
|