owpengram-server/internal/app/updates/service_test.go

611 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package updates
import (
"context"
"errors"
"testing"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func TestRecordNewMessageFeedsGetDifference(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 1
svc := NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore())
msg := domain.Message{
ID: 10,
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
Date: 1700000000,
Body: "Login code: 12345",
}
event, state, err := svc.RecordNewMessage(ctx, authKeyID, msg.OwnerUserID, msg)
if err != nil {
t.Fatalf("RecordNewMessage: %v", err)
}
if event.Pts != 1 || event.PtsCount != 1 || state.Pts != 1 || state.Seq != 0 {
t.Fatalf("event/state = %+v / %+v, want first pts event with seq=0", event, state)
}
diff, err := svc.GetDifference(ctx, authKeyID, msg.OwnerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State != state || len(diff.Events) != 1 || diff.Events[0].Message.ID != msg.ID {
t.Fatalf("diff = %+v, want recorded login message event and state %+v", diff, state)
}
diff, err = svc.GetDifference(ctx, authKeyID, msg.OwnerUserID, state)
if err != nil {
t.Fatalf("GetDifference current: %v", err)
}
if len(diff.Events) != 0 || diff.State != state {
t.Fatalf("current diff = %+v, want empty events and same state", diff)
}
}
func TestPublishNewMessageIsIdempotentByMessageBoxID(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 11
events := memory.NewUpdateEventStore()
svc := NewService(memory.NewUpdateStateStore(), events)
msg := domain.Message{
ID: 10,
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
Date: 1700000000,
Body: "Login code: 12345",
}
firstEvent, firstState, err := svc.PublishNewMessage(ctx, msg.OwnerUserID, msg)
if err != nil {
t.Fatalf("PublishNewMessage first: %v", err)
}
secondEvent, secondState, err := svc.PublishNewMessage(ctx, msg.OwnerUserID, msg)
if err != nil {
t.Fatalf("PublishNewMessage retry: %v", err)
}
if firstEvent.Pts != 1 || firstState.Pts != 1 {
t.Fatalf("first event/state = %+v / %+v, want pts=1", firstEvent, firstState)
}
if secondEvent.Pts != firstEvent.Pts || secondState.Pts != firstState.Pts {
t.Fatalf("retry event/state = %+v / %+v, want same pts as first %+v / %+v", secondEvent, secondState, firstEvent, firstState)
}
diff, err := svc.GetDifference(ctx, authKeyID, msg.OwnerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 1 || len(diff.Events) != 1 || diff.Events[0].Message.ID != msg.ID {
t.Fatalf("diff = %+v, want one durable login message event", diff)
}
}
func TestRecordReadHistoryFeedsGetDifference(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 2
svc := NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore())
peer := domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID}
ownerUserID := int64(1000000001)
event, state, err := svc.RecordReadHistory(ctx, authKeyID, ownerUserID, domain.ReadHistoryResult{
OwnerUserID: ownerUserID,
Peer: peer,
MaxID: 10,
Changed: true,
}, 0)
if err != nil {
t.Fatalf("RecordReadHistory: %v", err)
}
if event.Type != domain.UpdateEventReadHistoryInbox || event.Pts != 1 || event.PtsCount != 1 || state.Pts != 1 {
t.Fatalf("event/state = %+v / %+v, want read history event with first pts", event, state)
}
diff, err := svc.GetDifference(ctx, authKeyID, ownerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State != state || len(diff.Events) != 1 || diff.Events[0].Peer != peer || diff.Events[0].MaxID != 10 {
t.Fatalf("diff = %+v, want recorded read history event and state %+v", diff, state)
}
}
func TestRecordChannelReadHistoryKeepsChannelPtsPayload(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 7
svc := NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore())
ownerUserID := int64(1000000001)
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: 7001}
event, state, err := svc.RecordReadHistory(ctx, authKeyID, ownerUserID, domain.ReadHistoryResult{
OwnerUserID: ownerUserID,
Peer: peer,
MaxID: 11,
StillUnreadCount: 3,
ChannelPts: 77,
Changed: true,
}, 0)
if err != nil {
t.Fatalf("RecordReadHistory: %v", err)
}
if event.Pts != 1 || state.Pts != 1 || event.ChannelPts != 77 {
t.Fatalf("event/state = %+v / %+v, want account pts=1 and channel pts payload=77", event, state)
}
diff, err := svc.GetDifference(ctx, authKeyID, ownerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if len(diff.Events) != 1 || diff.Events[0].Peer != peer || diff.Events[0].ChannelPts != 77 {
t.Fatalf("diff = %+v, want recorded channel read payload with channel pts=77", diff)
}
}
func TestRecordSettingsEventsFeedGetDifference(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 3
svc := NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore())
ownerUserID := int64(1000000001)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
if _, _, err := svc.RecordContactsReset(ctx, authKeyID, ownerUserID, 0); err != nil {
t.Fatalf("RecordContactsReset: %v", err)
}
if _, _, err := svc.RecordDialogPinned(ctx, authKeyID, ownerUserID, peer, true, 0, 0); err != nil {
t.Fatalf("RecordDialogPinned: %v", err)
}
order := []domain.Peer{peer}
if _, _, err := svc.RecordPinnedDialogs(ctx, authKeyID, ownerUserID, 0, order, 0); err != nil {
t.Fatalf("RecordPinnedDialogs: %v", err)
}
if _, _, err := svc.RecordDialogUnreadMark(ctx, authKeyID, ownerUserID, peer, false, 0); err != nil {
t.Fatalf("RecordDialogUnreadMark: %v", err)
}
settings := domain.PeerSettings{ShareContact: true}
if _, _, err := svc.RecordPeerSettings(ctx, authKeyID, ownerUserID, peer, settings, 0); err != nil {
t.Fatalf("RecordPeerSettings: %v", err)
}
stateEvent, state, err := svc.RecordPeerStoryBlocked(ctx, authKeyID, ownerUserID, peer, true, 0)
if err != nil {
t.Fatalf("RecordPeerStoryBlocked: %v", err)
}
if stateEvent.Pts != 6 || state.Pts != 6 {
t.Fatalf("last event/state = %+v / %+v, want pts=6", stateEvent, state)
}
diff, err := svc.GetDifference(ctx, authKeyID, ownerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 6 || len(diff.Events) != 6 {
t.Fatalf("diff = %+v, want six settings events", diff)
}
wantTypes := []domain.UpdateEventType{
domain.UpdateEventContactsReset,
domain.UpdateEventDialogPinned,
domain.UpdateEventPinnedDialogs,
domain.UpdateEventDialogUnreadMark,
domain.UpdateEventPeerSettings,
domain.UpdateEventPeerStoryBlocked,
}
for i, typ := range wantTypes {
if diff.Events[i].Type != typ || diff.Events[i].Pts != i+1 || diff.Events[i].PtsCount != 1 {
t.Fatalf("event[%d] = %+v, want type=%s pts=%d pts_count=1", i, diff.Events[i], typ, i+1)
}
}
if diff.Events[1].Peer != peer || !diff.Events[1].Bool {
t.Fatalf("dialog pinned event = %+v, want peer and pinned=true", diff.Events[1])
}
if diff.Events[3].Peer != peer || diff.Events[3].Bool {
t.Fatalf("unread mark event = %+v, want peer and unread=false", diff.Events[3])
}
if len(diff.Events[2].Peers) != 1 || diff.Events[2].Peers[0] != peer {
t.Fatalf("pinned dialogs event = %+v, want order peer", diff.Events[2])
}
if diff.Events[4].Peer != peer || !diff.Events[4].Settings.ShareContact {
t.Fatalf("peer settings event = %+v, want peer and settings", diff.Events[4])
}
if diff.Events[5].Peer != peer || !diff.Events[5].Bool {
t.Fatalf("peer story blocked event = %+v, want peer and blocked=true", diff.Events[5])
}
}
func TestRecordSettingsEventUsesDispatchAppender(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 4
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
event, state, err := svc.RecordDialogPinned(ctx, authKeyID, 1000000001, peer, true, 0, 42)
if err != nil {
t.Fatalf("RecordDialogPinned: %v", err)
}
if event.Pts != 1 || state.Pts != 1 {
t.Fatalf("event/state = %+v / %+v, want first pts", event, state)
}
if !events.dispatched || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 42 || events.event.Type != domain.UpdateEventDialogPinned || events.event.Peer != peer {
t.Fatalf("dispatch capture = %+v exclude_auth=%v exclude_session=%d dispatched=%v, want dialog_pinned outbox", events.event, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
}
func TestRecordSettingsEventDispatchFailureDoesNotRecordEvent(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 6
events := &failingDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
_, _, err := svc.RecordDialogPinned(ctx, authKeyID, 1000000001, domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}, true, 0, 42)
if !errors.Is(err, errDispatchFailed) {
t.Fatalf("RecordDialogPinned err = %v, want dispatch failure", err)
}
diff, err := svc.GetDifference(ctx, authKeyID, 1000000001, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 0 || len(diff.Events) != 0 {
t.Fatalf("diff after dispatch failure = %+v, want no durable event before allocated append commits", diff)
}
}
func TestRecordPeerStoryBlockedUsesDispatchAppender(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 7
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
event, state, err := svc.RecordPeerStoryBlocked(ctx, authKeyID, 1000000001, peer, true, 91)
if err != nil {
t.Fatalf("RecordPeerStoryBlocked: %v", err)
}
if event.Pts != 1 || state.Pts != 1 || !event.LacksWirePts() {
t.Fatalf("event/state = %+v / %+v, want first aux pts event", event, state)
}
if !events.dispatched || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 91 || events.event.Type != domain.UpdateEventPeerStoryBlocked || events.event.Peer != peer || !events.event.Bool {
t.Fatalf("dispatch capture = %+v exclude_auth=%v exclude_session=%d dispatched=%v, want peer_story_blocked outbox", events.event, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
}
func TestRecordStoryUsesDispatchAppenderExcludeCurrentSession(t *testing.T) {
ctx := context.Background()
authKeyID := [8]byte{8, 1, 0}
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001}
story := domain.Story{
Owner: owner,
ID: 3,
Date: 1700000100,
ExpireDate: 1700086500,
Public: true,
Caption: "owner story",
}
event, state, err := svc.RecordStory(ctx, authKeyID, owner.ID, story, 1234)
if err != nil {
t.Fatalf("RecordStory: %v", err)
}
if event.Type != domain.UpdateEventStory || event.Pts != 1 || event.PtsCount != 1 || state.Pts != 1 {
t.Fatalf("event/state = %+v / %+v, want first story pts event", event, state)
}
if !events.dispatched || events.userID != owner.ID || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 1234 {
t.Fatalf("dispatch capture = user %d exclude_auth=%v exclude_session=%d dispatched=%v, want current session excluded", events.userID, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
if events.event.Type != domain.UpdateEventStory || events.event.Peer != owner || events.event.Story.ID != story.ID {
t.Fatalf("dispatch event = %+v, want story update for owner story", events.event)
}
}
func TestRecordStoryReadAndSentReactionExcludeCurrentSession(t *testing.T) {
ctx := context.Background()
authKeyID := [8]byte{8, 1, 4}
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
viewerID := int64(1000000002)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001}
story := domain.Story{
Owner: peer,
ID: 5,
Date: 1700000200,
ExpireDate: 1700086600,
Public: true,
}
event, state, err := svc.RecordReadStories(ctx, authKeyID, viewerID, domain.StoryReadResult{
ViewerID: viewerID,
Peer: peer,
MaxReadID: story.ID,
Advanced: true,
Date: 1700000201,
}, 2233)
if err != nil {
t.Fatalf("RecordReadStories: %v", err)
}
if event.Type != domain.UpdateEventReadStories || event.Pts != 1 || state.Pts != 1 {
t.Fatalf("read event/state = %+v / %+v, want first read story pts event", event, state)
}
if !events.dispatched || events.userID != viewerID || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 2233 || events.event.Type != domain.UpdateEventReadStories || events.event.MaxID != story.ID {
t.Fatalf("read dispatch capture = %+v user %d exclude_auth=%v exclude_session=%d dispatched=%v, want current session excluded", events.event, events.userID, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
reaction := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "🔥"}
event, state, err = svc.RecordSentStoryReaction(ctx, authKeyID, viewerID, domain.StoryReactionResult{
ViewerID: viewerID,
Peer: peer,
StoryID: story.ID,
Story: story,
Reaction: reaction,
Changed: true,
Date: 1700000202,
}, 2233)
if err != nil {
t.Fatalf("RecordSentStoryReaction: %v", err)
}
if event.Type != domain.UpdateEventSentStoryReaction || event.Pts != 2 || state.Pts != 2 {
t.Fatalf("reaction event/state = %+v / %+v, want second sent story reaction pts event", event, state)
}
if !events.dispatched || events.userID != viewerID || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 2233 || events.event.Type != domain.UpdateEventSentStoryReaction || events.event.Reaction == nil || events.event.Reaction.Emoticon != "🔥" {
t.Fatalf("reaction dispatch capture = %+v user %d exclude_auth=%v exclude_session=%d dispatched=%v, want current session excluded", events.event, events.userID, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
}
func TestRecordNewStoryReactionDispatchesWithoutSavingDeviceState(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 5
states := &captureStateStore{}
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(states, events)
ownerID := int64(1000000001)
viewerID := int64(1000000002)
reaction := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "👍"}
event, state, err := svc.RecordNewStoryReaction(ctx, authKeyID, 0, domain.StoryReactionResult{
ViewerID: viewerID,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: ownerID},
StoryID: 3,
Story: domain.Story{
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: ownerID},
ID: 3,
Date: 1700000100,
},
Reaction: reaction,
Date: 1700000101,
}, 0)
if err != nil {
t.Fatalf("RecordNewStoryReaction: %v", err)
}
if event.Type != domain.UpdateEventNewStoryReaction || event.UserID != ownerID || event.Peer.ID != viewerID || event.Reaction == nil || event.Reaction.Emoticon != "👍" {
t.Fatalf("event = %+v, want owner-side new story reaction from viewer", event)
}
if state.Pts != 1 || state.Seq != 0 {
t.Fatalf("state = %+v, want first account pts", state)
}
if states.saveCount != 0 {
t.Fatalf("state saves = %d, want no device state save for remote owner notification", states.saveCount)
}
if !events.dispatched || events.userID != ownerID || events.event.Type != domain.UpdateEventNewStoryReaction {
t.Fatalf("dispatch capture = %+v user=%d dispatched=%v, want owner outbox event", events.event, events.userID, events.dispatched)
}
diff, err := svc.GetDifference(ctx, authKeyID, ownerID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 1 || len(diff.Events) != 1 || diff.Events[0].Type != domain.UpdateEventNewStoryReaction {
t.Fatalf("diff = %+v, want one durable new story reaction", diff)
}
}
func TestClearAuthKeyDropsStateAndEvents(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 8
states := memory.NewUpdateStateStore()
events := memory.NewUpdateEventStore()
svc := NewService(states, events)
msg := domain.Message{
ID: 1,
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
Date: 1700000000,
}
if _, _, err := svc.RecordNewMessage(ctx, authKeyID, msg.OwnerUserID, msg); err != nil {
t.Fatalf("RecordNewMessage: %v", err)
}
if err := svc.ClearAuthKey(ctx, authKeyID); err != nil {
t.Fatalf("ClearAuthKey: %v", err)
}
diff, err := svc.GetDifference(ctx, authKeyID, msg.OwnerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 1 || len(diff.Events) != 1 {
t.Fatalf("difference after clear = %+v, want durable user events to remain", diff)
}
diff, err = svc.GetDifference(ctx, authKeyID, msg.OwnerUserID+1, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference other user: %v", err)
}
if diff.State.Pts != 0 || len(diff.Events) != 0 {
t.Fatalf("difference for other user after clear = %+v, want no cross-account events", diff)
}
}
func TestDeleteMessagesPtsRangeFeedsGetDifference(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 9
userID := int64(1000000001)
events := memory.NewUpdateEventStore()
svc := NewService(memory.NewUpdateStateStore(), events)
for _, event := range []domain.UpdateEvent{
{UserID: userID, Type: domain.UpdateEventNewMessage, Pts: 1, PtsCount: 1, Date: 1700000001, Message: domain.Message{ID: 1, OwnerUserID: userID}},
{UserID: userID, Type: domain.UpdateEventNewMessage, Pts: 2, PtsCount: 1, Date: 1700000002, Message: domain.Message{ID: 2, OwnerUserID: userID}},
{UserID: userID, Type: domain.UpdateEventDeleteMessages, Pts: 4, PtsCount: 2, Date: 1700000003, MessageIDs: []int{1, 2}},
} {
if err := events.Append(ctx, userID, event); err != nil {
t.Fatalf("append event pts=%d: %v", event.Pts, err)
}
}
state, err := svc.GetState(ctx, authKeyID, userID)
if err != nil {
t.Fatalf("GetState: %v", err)
}
if state.Pts != 4 {
t.Fatalf("state = %+v, want contiguous pts=4 across delete range", state)
}
diff, err := svc.GetDifference(ctx, authKeyID, userID, domain.UpdateState{Pts: 2})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 4 || len(diff.Events) != 1 {
t.Fatalf("diff = %+v, want one delete event ending at pts=4", diff)
}
got := diff.Events[0]
if got.Type != domain.UpdateEventDeleteMessages || got.Pts != 4 || got.PtsCount != 2 || len(got.MessageIDs) != 2 {
t.Fatalf("delete event = %+v, want pts=4 pts_count=2 ids", got)
}
}
// TestAcknowledgeCurrentStateAdvancesConfirmedWatermark 验证 updates.getState
// 的语义:返回账号当前最新连续 pts而非设备旧确认水位并把确认水位推进
// 到此——TDesktop 不持久化 pts启动靠 getState+getDialogs 快照对齐,返回旧
// 水位会诱导其重放快照前差分未读重复累计、dialog 预览被旧消息抢占)。
func TestAcknowledgeCurrentStateAdvancesConfirmedWatermark(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 11
userID := int64(1000000001)
events := memory.NewUpdateEventStore()
svc := NewService(memory.NewUpdateStateStore(), events)
if err := events.Append(ctx, userID, domain.UpdateEvent{
UserID: userID, Type: domain.UpdateEventNewMessage, Pts: 1, PtsCount: 1,
Date: 1700000001, Message: domain.Message{ID: 1, OwnerUserID: userID},
}); err != nil {
t.Fatalf("append: %v", err)
}
// 设备确认水位停在 pts=1 后账号又推进两格。
if _, err := svc.GetDifference(ctx, authKeyID, userID, domain.UpdateState{Pts: 1}); err != nil {
t.Fatalf("GetDifference: %v", err)
}
for pts := 2; pts <= 3; pts++ {
if err := events.Append(ctx, userID, domain.UpdateEvent{
UserID: userID, Type: domain.UpdateEventNewMessage, Pts: pts, PtsCount: 1,
Date: 1700000001 + pts, Message: domain.Message{ID: pts, OwnerUserID: userID},
}); err != nil {
t.Fatalf("append pts=%d: %v", pts, err)
}
}
st, err := svc.AcknowledgeCurrentState(ctx, authKeyID, userID)
if err != nil {
t.Fatalf("AcknowledgeCurrentState: %v", err)
}
if st.Pts != 3 {
t.Fatalf("acknowledged state pts = %d, want account current 3", st.Pts)
}
confirmed, err := svc.GetState(ctx, authKeyID, userID)
if err != nil {
t.Fatalf("GetState after acknowledge: %v", err)
}
if confirmed.Pts != 3 {
t.Fatalf("confirmed watermark = %d, want advanced to 3", confirmed.Pts)
}
}
type captureDispatchAppender struct {
*memory.UpdateEventStore
dispatched bool
userID int64
event domain.UpdateEvent
excludeAuthKeyID [8]byte
excludeSessionID int64
}
func (s *captureDispatchAppender) AppendAllocatedWithDispatch(ctx context.Context, userID int64, event domain.UpdateEvent, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, error) {
s.dispatched = true
s.userID = userID
s.excludeAuthKeyID = excludeAuthKeyID
s.excludeSessionID = excludeSessionID
event, err := s.UpdateEventStore.AppendAllocated(ctx, userID, event)
s.event = event
return event, err
}
var errDispatchFailed = errors.New("dispatch failed")
type failingDispatchAppender struct {
*memory.UpdateEventStore
}
func (s *failingDispatchAppender) AppendAllocatedWithDispatch(context.Context, int64, domain.UpdateEvent, [8]byte, int64) (domain.UpdateEvent, error) {
return domain.UpdateEvent{}, errDispatchFailed
}
type captureStateStore struct {
saveCount int
states map[[16]byte]domain.UpdateState
}
func (s *captureStateStore) Get(_ context.Context, authKeyID [8]byte, userID int64) (domain.UpdateState, bool, error) {
if s.states == nil {
return domain.UpdateState{}, false, nil
}
st, ok := s.states[captureStateKey(authKeyID, userID)]
return st, ok, nil
}
func (s *captureStateStore) Save(_ context.Context, authKeyID [8]byte, userID int64, state domain.UpdateState) error {
if s.states == nil {
s.states = make(map[[16]byte]domain.UpdateState)
}
s.saveCount++
s.states[captureStateKey(authKeyID, userID)] = state
return nil
}
func (s *captureStateStore) Delete(_ context.Context, authKeyID [8]byte, userID int64) error {
if s.states != nil {
delete(s.states, captureStateKey(authKeyID, userID))
}
return nil
}
func (s *captureStateStore) DeleteAuthKey(_ context.Context, authKeyID [8]byte) error {
if s.states == nil {
return nil
}
for key := range s.states {
var got [8]byte
copy(got[:], key[:8])
if got == authKeyID {
delete(s.states, key)
}
}
return nil
}
func captureStateKey(authKeyID [8]byte, userID int64) [16]byte {
var key [16]byte
copy(key[:8], authKeyID[:])
for i := 0; i < 8; i++ {
key[8+i] = byte(userID >> (8 * i))
}
return key
}