owpengram-server/internal/store/memory/login_code_delivery_test.go

165 lines
6 KiB
Go

package memory
import (
"context"
"errors"
"reflect"
"sync"
"sync/atomic"
"testing"
"telesrv/internal/domain"
)
func TestLoginCodeDeliveryStoreCommitsMessageEventDialogAndReplay(t *testing.T) {
ctx := context.Background()
const userID int64 = 1000000001
dialogs := NewDialogStore()
messages := NewMessageStore(dialogs)
events := NewUpdateEventStore()
deliveries := NewLoginCodeDeliveryStore(messages, events)
req := domain.LoginCodeDeliveryRequest{
UserID: userID,
PhoneCodeHash: "phone-code-hash-one",
Code: "12345",
Date: 1700000000,
ExpiresAt: 1700000300,
}
first, err := deliveries.DeliverLoginCodeMessage(ctx, req)
if err != nil {
t.Fatalf("DeliverLoginCodeMessage: %v", err)
}
if !first.Created || first.Message.ID != 1 || first.Message.UID != 1 || first.Message.Pts != 1 || first.Message.Out ||
first.Message.OwnerUserID != userID || first.Message.Peer.ID != domain.OfficialSystemUserID || first.Message.From.ID != domain.OfficialSystemUserID {
t.Fatalf("first delivery = %+v, want first incoming 777000 message", first)
}
if len(messages.m[userID]) != 1 || !reflect.DeepEqual(messages.m[userID][0], first.Message) {
t.Fatalf("message projection = %+v, want committed message", messages.m[userID])
}
if len(events.events[userID]) != 1 {
t.Fatalf("durable events = %+v, want one new_message", events.events[userID])
}
event := events.events[userID][0]
if event.Type != domain.UpdateEventNewMessage || event.Pts != first.Message.Pts || event.PtsCount != 1 || !reflect.DeepEqual(event.Message, first.Message) {
t.Fatalf("event = %+v, want message-identical new_message", event)
}
list := dialogs.m[userID]
if len(list.Dialogs) != 1 || list.Dialogs[0].Peer.ID != domain.OfficialSystemUserID || list.Dialogs[0].TopMessage != first.Message.ID || list.Dialogs[0].UnreadCount != 1 {
t.Fatalf("dialog projection = %+v, want unread 777000 dialog", list.Dialogs)
}
if len(list.Users) != 1 || list.Users[0].ID != domain.OfficialSystemUserID {
t.Fatalf("dialog users = %+v, want official system user", list.Users)
}
replayReq := req
replayReq.Date++
replay, err := deliveries.DeliverLoginCodeMessage(ctx, replayReq)
if err != nil {
t.Fatalf("replay DeliverLoginCodeMessage: %v", err)
}
if replay.Created || !reflect.DeepEqual(replay.Message, first.Message) {
t.Fatalf("replay = %+v, want immutable first result %+v", replay, first)
}
if len(messages.m[userID]) != 1 || len(events.events[userID]) != 1 || len(messages.loginCodeDeliveries) != 1 {
t.Fatalf("replay created facts: messages=%d events=%d receipts=%d", len(messages.m[userID]), len(events.events[userID]), len(messages.loginCodeDeliveries))
}
second, err := deliveries.DeliverLoginCodeMessage(ctx, domain.LoginCodeDeliveryRequest{
UserID: userID,
PhoneCodeHash: "phone-code-hash-two",
Code: "67890",
Date: 1700000010,
ExpiresAt: 1700000310,
})
if err != nil {
t.Fatalf("second distinct delivery: %v", err)
}
if !second.Created || second.Message.ID != 2 || second.Message.UID != 2 || second.Message.Pts != 2 || len(events.events[userID]) != 2 {
t.Fatalf("second delivery = %+v events=%+v, want contiguous allocations", second, events.events[userID])
}
if got := dialogs.m[userID].Dialogs[0].UnreadCount; got != 2 {
t.Fatalf("dialog unread = %d, want 2", got)
}
}
func TestLoginCodeDeliveryStoreConcurrentReplayAndConflict(t *testing.T) {
ctx := context.Background()
const userID int64 = 1000000002
messages := NewMessageStore(NewDialogStore())
events := NewUpdateEventStore()
deliveries := NewLoginCodeDeliveryStore(messages, events)
req := domain.LoginCodeDeliveryRequest{
UserID: userID,
PhoneCodeHash: "concurrent-phone-code-hash",
Code: "24680",
Date: 1700000100,
ExpiresAt: 1700000400,
}
const workers = 32
var created atomic.Int32
results := make(chan domain.LoginCodeDeliveryResult, workers)
errs := make(chan error, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
got, err := deliveries.DeliverLoginCodeMessage(ctx, req)
if err != nil {
errs <- err
return
}
if got.Created {
created.Add(1)
}
results <- got
}()
}
wg.Wait()
close(errs)
close(results)
for err := range errs {
t.Fatalf("concurrent delivery: %v", err)
}
if created.Load() != 1 {
t.Fatalf("created calls = %d, want exactly 1", created.Load())
}
for got := range results {
if got.Message.ID != 1 || got.Message.UID != 1 || got.Message.Pts != 1 {
t.Fatalf("concurrent result = %+v, want the same first allocation", got)
}
}
if len(messages.m[userID]) != 1 || len(events.events[userID]) != 1 || len(messages.loginCodeDeliveries) != 1 {
t.Fatalf("concurrent facts: messages=%d events=%d receipts=%d", len(messages.m[userID]), len(events.events[userID]), len(messages.loginCodeDeliveries))
}
changedCode := req
changedCode.Code = "13579"
if _, err := deliveries.DeliverLoginCodeMessage(ctx, changedCode); !errors.Is(err, domain.ErrLoginCodeDeliveryConflict) {
t.Fatalf("changed-code replay err = %v, want ErrLoginCodeDeliveryConflict", err)
}
changedUser := req
changedUser.UserID++
if _, err := deliveries.DeliverLoginCodeMessage(ctx, changedUser); !errors.Is(err, domain.ErrLoginCodeDeliveryConflict) {
t.Fatalf("changed-user replay err = %v, want ErrLoginCodeDeliveryConflict", err)
}
if len(messages.m[userID]) != 1 || len(events.events[userID]) != 1 {
t.Fatal("conflicting replay changed committed facts")
}
}
func TestLoginCodeDeliveryStoreRequiresSharedEventStore(t *testing.T) {
messages := NewMessageStore()
_, err := NewLoginCodeDeliveryStore(messages, nil).DeliverLoginCodeMessage(context.Background(), domain.LoginCodeDeliveryRequest{
UserID: 1000000003,
PhoneCodeHash: "missing-event-store",
Code: "12345",
Date: 1700000200,
ExpiresAt: 1700000500,
})
if !errors.Is(err, domain.ErrLoginCodeDeliveryInvalid) {
t.Fatalf("missing event store err = %v, want ErrLoginCodeDeliveryInvalid", err)
}
}